Skip to content

Commit 56ca073

Browse files
DeviceInfracopybara-github
DeviceInfra
authored andcommitted
Internal change
PiperOrigin-RevId: 742604817
1 parent 46ab282 commit 56ca073

File tree

4 files changed

+329
-1
lines changed

4 files changed

+329
-1
lines changed

src/java/com/google/devtools/mobileharness/shared/util/file/local/BUILD

+6
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,9 @@ java_library(
5454
"@maven//:com_google_guava_guava",
5555
],
5656
)
57+
58+
java_library(
59+
name = "unzip_file_name",
60+
srcs = ["UnzipFileName.java"],
61+
deps = ["@maven//:com_google_guava_guava"],
62+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.devtools.mobileharness.shared.util.file.local;
18+
19+
import com.google.common.collect.ImmutableList;
20+
21+
/**
22+
* A class to represent a file name argument to the unzip command.
23+
*
24+
* <p>See `$ man unzip` for more details.
25+
*/
26+
public class UnzipFileName {
27+
private final String arg;
28+
private final ImmutableList<Token> tokens;
29+
30+
/**
31+
* Constructs an UnzipFileName from the given argument.
32+
*
33+
* @param arg the raw command line argument to the unzip command.
34+
*/
35+
public UnzipFileName(String arg) {
36+
this.arg = arg;
37+
this.tokens = this.parse();
38+
}
39+
40+
/** Constructs a list of UnzipFileName from the given arguments. */
41+
public static ImmutableList<UnzipFileName> listOf(String... args) {
42+
ImmutableList.Builder<UnzipFileName> builder = ImmutableList.builder();
43+
for (String arg : args) {
44+
builder.add(new UnzipFileName(arg));
45+
}
46+
return builder.build();
47+
}
48+
49+
@Override
50+
public boolean equals(Object o) {
51+
if (this == o) {
52+
return true;
53+
}
54+
if (o instanceof UnzipFileName) {
55+
UnzipFileName that = (UnzipFileName) o;
56+
return arg.equals(that.arg);
57+
}
58+
return false;
59+
}
60+
61+
@Override
62+
public int hashCode() {
63+
return arg.hashCode();
64+
}
65+
66+
private enum Kind {
67+
CHAR,
68+
WILDCARD,
69+
IN_BRACKET
70+
}
71+
72+
private static class Token {
73+
final Kind kind;
74+
final char ch;
75+
final String regex;
76+
77+
Token(Kind kind, char ch, String regex) {
78+
this.kind = kind;
79+
this.ch = ch;
80+
this.regex = regex;
81+
}
82+
83+
static Token literal(char ch) {
84+
return new Token(Kind.CHAR, ch, "");
85+
}
86+
87+
static Token wildcard(char ch, String regex) {
88+
return new Token(Kind.WILDCARD, ch, regex);
89+
}
90+
91+
static Token bracketNegation(char ch) {
92+
return new Token(Kind.IN_BRACKET, ch, "^");
93+
}
94+
95+
static Token bracketChar(char ch) {
96+
if (ch == '\\' || ch == ']' || ch == '^') { // they have special meaning in brackets
97+
return new Token(Kind.IN_BRACKET, ch, "\\" + ch);
98+
}
99+
return new Token(Kind.IN_BRACKET, ch, Character.toString(ch));
100+
}
101+
}
102+
103+
private ImmutableList<Token> parse() {
104+
ImmutableList.Builder<Token> builder = ImmutableList.builder();
105+
for (int i = 0; i < arg.length(); ++i) {
106+
char ch = arg.charAt(i);
107+
switch (ch) {
108+
case '\\':
109+
if (++i < arg.length()) {
110+
builder.add(Token.literal(arg.charAt(i)));
111+
} // else invalid escape, discarded
112+
break;
113+
case '*':
114+
builder.add(Token.wildcard(ch, ".*"));
115+
break;
116+
case '?':
117+
builder.add(Token.wildcard(ch, "."));
118+
break;
119+
case '[':
120+
i = parseBracket(i, builder);
121+
break;
122+
default:
123+
builder.add(Token.literal(ch));
124+
}
125+
}
126+
return builder.build();
127+
}
128+
129+
private int parseBracket(int start, ImmutableList.Builder<Token> builder) {
130+
builder.add(Token.wildcard('[', "["));
131+
int i = start + 1;
132+
for (; i < arg.length(); ++i) {
133+
char ch = arg.charAt(i);
134+
switch (ch) {
135+
case '\\':
136+
if (++i < arg.length()) {
137+
builder.add(Token.bracketChar(arg.charAt(i)));
138+
} // else invalid escape, discarded
139+
break;
140+
case ']':
141+
builder.add(Token.wildcard(ch, "]"));
142+
return i;
143+
case '!':
144+
case '^':
145+
if (i == start + 1) {
146+
builder.add(Token.bracketNegation(ch));
147+
} else {
148+
builder.add(Token.bracketChar(ch));
149+
}
150+
break;
151+
default:
152+
builder.add(Token.bracketChar(ch));
153+
}
154+
}
155+
return i;
156+
}
157+
158+
/** Returns the raw command line argument to the unzip command. */
159+
public String arg() {
160+
return arg;
161+
}
162+
163+
/** Returns true if the name contains any unescaped '*', '?' or '['. */
164+
public boolean hasWildcard() {
165+
return tokens.stream().anyMatch(token -> token.kind == Kind.WILDCARD);
166+
}
167+
168+
/**
169+
* Returns the unescaped literal path.
170+
*
171+
* <p>Any wildcards will be kept as is. So this is inappropriate for use if hasWildcard() is true.
172+
*/
173+
public String path() {
174+
StringBuilder builder = new StringBuilder();
175+
tokens.forEach(token -> builder.append(token.ch));
176+
return builder.toString();
177+
}
178+
179+
/** A builder used by the regex() method to manage \Q...\E quoting. */
180+
private static class RegexBuilder {
181+
private final StringBuilder builder = new StringBuilder();
182+
private boolean inQuote = false;
183+
184+
void appendQuote(char c) {
185+
if (!inQuote) {
186+
builder.append("\\Q");
187+
inQuote = true;
188+
}
189+
builder.append(c);
190+
}
191+
192+
void appendRegex(String w) {
193+
if (inQuote) {
194+
builder.append("\\E");
195+
inQuote = false;
196+
}
197+
builder.append(w);
198+
}
199+
200+
String regex() {
201+
return builder.toString();
202+
}
203+
}
204+
205+
/**
206+
* Returns the equivalent regular expression.
207+
*
208+
* <p>The return value will be passed to a Go binary, so the Go regex syntax is used. See
209+
* http://godoc/pkg/regexp/syntax.
210+
*
211+
* <p>Invalid '[...]' wildcards will convert to invalid regular expressions.
212+
*/
213+
public String regex() {
214+
UnzipFileName.RegexBuilder builder = new UnzipFileName.RegexBuilder();
215+
builder.appendRegex("^");
216+
for (Token token : tokens) {
217+
if (token.kind == Kind.CHAR) {
218+
builder.appendQuote(token.ch);
219+
} else {
220+
builder.appendRegex(token.regex);
221+
}
222+
}
223+
builder.appendRegex("$");
224+
return builder.regex();
225+
}
226+
}

src/javatests/com/google/devtools/mobileharness/shared/util/file/local/BUILD

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
#
1515

16-
load("@rules_java//java:defs.bzl", "java_library")
16+
load("@rules_java//java:defs.bzl", "java_library", "java_test")
1717

1818
package(
1919
default_applicable_licenses = ["//:license"],
@@ -33,3 +33,14 @@ java_library(
3333
"@maven//:com_google_guava_guava",
3434
],
3535
)
36+
37+
java_test(
38+
name = "UnzipFileNameTest",
39+
srcs = ["UnzipFileNameTest.java"],
40+
deps = [
41+
"//src/java/com/google/devtools/mobileharness/shared/util/file/local:unzip_file_name",
42+
"//src/javatests/com/google/devtools/mobileharness/builddefs:truth",
43+
"@maven//:com_google_testparameterinjector_test_parameter_injector",
44+
"@maven//:junit_junit",
45+
],
46+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.devtools.mobileharness.shared.util.file.local;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
22+
import com.google.testing.junit.testparameterinjector.TestParameters;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
26+
@RunWith(TestParameterInjector.class)
27+
public final class UnzipFileNameTest {
28+
@Test
29+
// no special characters
30+
@TestParameters("{arg: 'foo_bar', expected: false}")
31+
// regex wildcard but not unzip wildcard
32+
@TestParameters("{arg: 'foo.bar', expected: false}")
33+
// unzip wildcards
34+
@TestParameters("{arg: 'foo*bar', expected: true}")
35+
@TestParameters("{arg: 'foo?bar', expected: true}")
36+
@TestParameters("{arg: 'foo[a-z]bar', expected: true}")
37+
// escaped wildcards
38+
@TestParameters("{arg: 'foo\\*bar', expected: false}")
39+
@TestParameters("{arg: 'foo\\?bar', expected: false}")
40+
@TestParameters("{arg: 'foo\\[a-z]bar', expected: false}")
41+
// escaped backslash
42+
@TestParameters("{arg: 'foo\\\\*bar', expected: true}")
43+
public void testHasWildcard(String arg, boolean expected) {
44+
UnzipFileName name = new UnzipFileName(arg);
45+
assertThat(name.hasWildcard()).isEqualTo(expected);
46+
}
47+
48+
@Test
49+
@TestParameters("{arg: 'foo.bar', expected: 'foo.bar'}")
50+
@TestParameters("{arg: 'foo*bar', expected: 'foo*bar'}")
51+
@TestParameters("{arg: 'foo\\*bar', expected: 'foo*bar'}")
52+
@TestParameters("{arg: 'foo\\\\*bar', expected: 'foo\\*bar'}")
53+
public void testUnescape(String arg, String expected) {
54+
UnzipFileName name = new UnzipFileName(arg);
55+
assertThat(name.path()).isEqualTo(expected);
56+
}
57+
58+
@Test
59+
// no wildcards
60+
@TestParameters("{arg: 'foo_bar', expected: '^\\Qfoo_bar\\E$'}")
61+
@TestParameters("{arg: 'foo.bar', expected: '^\\Qfoo.bar\\E$'}")
62+
// wildcards
63+
@TestParameters("{arg: 'foo*bar', expected: '^\\Qfoo\\E.*\\Qbar\\E$'}")
64+
@TestParameters("{arg: 'foo?bar', expected: '^\\Qfoo\\E.\\Qbar\\E$'}")
65+
@TestParameters("{arg: 'foo[a-z]bar', expected: '^\\Qfoo\\E[a-z]\\Qbar\\E$'}")
66+
// escaped wildcards
67+
@TestParameters("{arg: 'foo\\*bar', expected: '^\\Qfoo*bar\\E$'}")
68+
@TestParameters("{arg: 'foo\\?bar', expected: '^\\Qfoo?bar\\E$'}")
69+
@TestParameters("{arg: 'foo\\[a-z]bar', expected: '^\\Qfoo[a-z]bar\\E$'}")
70+
// escaped backslash
71+
@TestParameters("{arg: 'foo\\\\*bar', expected: '^\\Qfoo\\\\E.*\\Qbar\\E$'}")
72+
// brackets
73+
@TestParameters("{arg: '[]]', expected: '^[]\\Q]\\E$'}")
74+
@TestParameters("{arg: '[\\]]', expected: '^[\\]]$'}")
75+
@TestParameters("{arg: '[\\\\]', expected: '^[\\\\]$'}")
76+
@TestParameters("{arg: '[!!]', expected: '^[^!]$'}")
77+
@TestParameters("{arg: '[^^]', expected: '^[^\\^]$'}")
78+
@TestParameters("{arg: '[\\!]', expected: '^[!]$'}")
79+
@TestParameters("{arg: '[\\^]', expected: '^[\\^]$'}")
80+
@TestParameters("{arg: '[abc', expected: '^[abc$'}") // unclosed bracket
81+
public void testRegex(String arg, String expected) {
82+
UnzipFileName name = new UnzipFileName(arg);
83+
assertThat(name.regex()).isEqualTo(expected);
84+
}
85+
}

0 commit comments

Comments
 (0)