Skip to content

Commit 844197e

Browse files
DeviceInfracopybara-github
DeviceInfra
authored andcommitted
Internal change
PiperOrigin-RevId: 742604817
1 parent f743be3 commit 844197e

File tree

4 files changed

+347
-1
lines changed

4 files changed

+347
-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,243 @@
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.collect.ImmutableList.toImmutableList;
20+
import static java.util.Arrays.stream;
21+
import static java.util.stream.Collectors.joining;
22+
23+
import com.google.common.collect.ImmutableList;
24+
25+
/**
26+
* A class to represent a file name argument to the unzip command.
27+
*
28+
* <p>There are 3 different formats used in mobileharness:
29+
*
30+
* <p>1. Arguments to the unzip command. This is the input to the constructor and is returned by the
31+
* {@link #arg()} method. See `$ man unzip` for more details about the syntax itself.
32+
*
33+
* <p>2. File paths as in the file system. This is returned by the {@link #path()} method.
34+
*
35+
* <p>3. Arguments to the CAS downloader command as regular expressions. This is returned by the
36+
* {@link #regex()} method.
37+
*/
38+
public class UnzipFileName {
39+
private final String arg;
40+
private final ImmutableList<Token> tokens;
41+
42+
/**
43+
* Constructs an UnzipFileName from the given argument.
44+
*
45+
* @param arg the raw command line argument to the unzip command.
46+
*/
47+
public UnzipFileName(String arg) {
48+
this.arg = arg;
49+
this.tokens = this.parse();
50+
}
51+
52+
/** Constructs a list of UnzipFileName from the given arguments. */
53+
public static ImmutableList<UnzipFileName> listOf(String... args) {
54+
return stream(args).map(UnzipFileName::new).collect(toImmutableList());
55+
}
56+
57+
@Override
58+
public boolean equals(Object o) {
59+
if (this == o) {
60+
return true;
61+
}
62+
if (o instanceof UnzipFileName) {
63+
UnzipFileName that = (UnzipFileName) o;
64+
return arg.equals(that.arg);
65+
}
66+
return false;
67+
}
68+
69+
@Override
70+
public int hashCode() {
71+
return arg.hashCode();
72+
}
73+
74+
private enum Kind {
75+
CHAR,
76+
WILDCARD,
77+
IN_BRACKET
78+
}
79+
80+
private static class Token {
81+
final Kind kind;
82+
final char ch;
83+
final String regex;
84+
85+
private Token(Kind kind, char ch, String regex) {
86+
this.kind = kind;
87+
this.ch = ch;
88+
this.regex = regex;
89+
}
90+
91+
static Token literal(char ch) {
92+
return new Token(Kind.CHAR, ch, "");
93+
}
94+
95+
static Token wildcard(char ch, String regex) {
96+
return new Token(Kind.WILDCARD, ch, regex);
97+
}
98+
99+
static Token bracketNegation(char ch) {
100+
return new Token(Kind.IN_BRACKET, ch, "^");
101+
}
102+
103+
static Token bracketChar(char ch) {
104+
if (ch == '\\' || ch == ']' || ch == '^') { // they have special meaning in brackets
105+
return new Token(Kind.IN_BRACKET, ch, "\\" + ch);
106+
}
107+
return new Token(Kind.IN_BRACKET, ch, Character.toString(ch));
108+
}
109+
}
110+
111+
private ImmutableList<Token> parse() {
112+
ImmutableList.Builder<Token> builder = ImmutableList.builder();
113+
for (int i = 0; i < arg.length(); ++i) {
114+
char ch = arg.charAt(i);
115+
switch (ch) {
116+
case '\\':
117+
if (++i < arg.length()) {
118+
builder.add(Token.literal(arg.charAt(i)));
119+
} // else invalid escape, discarded
120+
break;
121+
case '*':
122+
builder.add(Token.wildcard(ch, ".*"));
123+
break;
124+
case '?':
125+
builder.add(Token.wildcard(ch, "."));
126+
break;
127+
case '[':
128+
i = parseBracket(i, builder);
129+
break;
130+
default:
131+
builder.add(Token.literal(ch));
132+
}
133+
}
134+
return builder.build();
135+
}
136+
137+
private int parseBracket(int start, ImmutableList.Builder<Token> builder) {
138+
builder.add(Token.wildcard('[', "["));
139+
int i = start + 1;
140+
for (; i < arg.length(); ++i) {
141+
char ch = arg.charAt(i);
142+
switch (ch) {
143+
case '\\':
144+
if (++i < arg.length()) {
145+
builder.add(Token.bracketChar(arg.charAt(i)));
146+
} // else invalid escape, discarded
147+
break;
148+
case ']':
149+
builder.add(Token.wildcard(ch, "]"));
150+
return i;
151+
case '!':
152+
case '^':
153+
if (i == start + 1) {
154+
builder.add(Token.bracketNegation(ch));
155+
} else {
156+
builder.add(Token.bracketChar(ch));
157+
}
158+
break;
159+
default:
160+
builder.add(Token.bracketChar(ch));
161+
}
162+
}
163+
return i;
164+
}
165+
166+
/** Returns the raw command line argument to the unzip command. */
167+
public String arg() {
168+
return arg;
169+
}
170+
171+
/** Returns true if the name contains any unescaped '*', '?' or '['. */
172+
public boolean hasWildcard() {
173+
return tokens.stream().anyMatch(token -> token.kind == Kind.WILDCARD);
174+
}
175+
176+
/**
177+
* Returns the unescaped literal path.
178+
*
179+
* <p>Any wildcards will be kept as is. So this is inappropriate for use if hasWildcard() is true.
180+
*/
181+
public String path() {
182+
return tokens.stream().map(t -> Character.toString(t.ch)).collect(joining());
183+
}
184+
185+
/** A builder used by the regex() method to manage \Q...\E quoting. */
186+
private static class RegexBuilder {
187+
private final StringBuilder builder = new StringBuilder();
188+
private boolean inQuote = false;
189+
190+
void beginQuote() {
191+
if (!inQuote) {
192+
builder.append("\\Q");
193+
inQuote = true;
194+
}
195+
}
196+
197+
void endQuote() {
198+
if (inQuote) {
199+
builder.append("\\E");
200+
inQuote = false;
201+
}
202+
}
203+
204+
void appendQuote(char c) {
205+
beginQuote();
206+
builder.append(c);
207+
if (c == '\\') {
208+
endQuote(); // in case there is a literal \E to match
209+
}
210+
}
211+
212+
void appendRegex(String w) {
213+
endQuote();
214+
builder.append(w);
215+
}
216+
217+
String regex() {
218+
return builder.toString();
219+
}
220+
}
221+
222+
/**
223+
* Returns the equivalent regular expression.
224+
*
225+
* <p>The return value will be passed to a Go binary, so the Go regex syntax is used. See
226+
* http://godoc/pkg/regexp/syntax.
227+
*
228+
* <p>Invalid '[...]' wildcards will convert to invalid regular expressions.
229+
*/
230+
public String regex() {
231+
UnzipFileName.RegexBuilder builder = new UnzipFileName.RegexBuilder();
232+
builder.appendRegex("^");
233+
for (Token token : tokens) {
234+
if (token.kind == Kind.CHAR) {
235+
builder.appendQuote(token.ch);
236+
} else {
237+
builder.appendRegex(token.regex);
238+
}
239+
}
240+
builder.appendRegex("$");
241+
return builder.regex();
242+
}
243+
}

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,86 @@
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+
@TestParameters("{arg: '\\\\E', expected: '^\\Q\\\\E\\QE\\E$'}")
73+
// brackets
74+
@TestParameters("{arg: '[]]', expected: '^[]\\Q]\\E$'}")
75+
@TestParameters("{arg: '[\\]]', expected: '^[\\]]$'}")
76+
@TestParameters("{arg: '[\\\\]', expected: '^[\\\\]$'}")
77+
@TestParameters("{arg: '[!!]', expected: '^[^!]$'}")
78+
@TestParameters("{arg: '[^^]', expected: '^[^\\^]$'}")
79+
@TestParameters("{arg: '[\\!]', expected: '^[!]$'}")
80+
@TestParameters("{arg: '[\\^]', expected: '^[\\^]$'}")
81+
@TestParameters("{arg: '[abc', expected: '^[abc$'}") // unclosed bracket
82+
public void testRegex(String arg, String expected) {
83+
UnzipFileName name = new UnzipFileName(arg);
84+
assertThat(name.regex()).isEqualTo(expected);
85+
}
86+
}

0 commit comments

Comments
 (0)