Skip to content

Commit 43f5d3d

Browse files
authored
Merge pull request #49 from openjavaformat/argfile-quotes
Read quoted arguments from parameter files, and refuse a file that includes itself
2 parents 9c98a31 + f5e6b12 commit 43f5d3d

2 files changed

Lines changed: 106 additions & 14 deletions

File tree

‎open-java-format/src/main/java/com/palantir/javaformat/java/CommandLineOptionsParser.java‎

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,51 @@
1414

1515
package com.palantir.javaformat.java;
1616

17-
import static java.nio.charset.StandardCharsets.UTF_8;
18-
19-
import com.google.common.base.CharMatcher;
17+
import com.google.common.base.Preconditions;
2018
import com.google.common.base.Splitter;
2119
import com.google.common.collect.ImmutableRangeSet;
2220
import com.google.common.collect.Range;
2321
import java.io.IOException;
2422
import java.io.UncheckedIOException;
2523
import java.nio.file.Files;
2624
import java.nio.file.Path;
27-
import java.nio.file.Paths;
25+
import java.util.ArrayDeque;
2826
import java.util.ArrayList;
27+
import java.util.Deque;
2928
import java.util.Iterator;
3029
import java.util.List;
30+
import java.util.regex.Matcher;
31+
import java.util.regex.Pattern;
3132
import javax.annotation.Nullable;
3233

3334
/** A parser for {@link CommandLineOptions}. */
3435
final class CommandLineOptionsParser {
3536

3637
private static final Splitter COMMA_SPLITTER = Splitter.on(',');
3738
private static final Splitter COLON_SPLITTER = Splitter.on(':');
38-
private static final Splitter ARG_SPLITTER =
39-
Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults();
39+
40+
/**
41+
* Splits the arguments of a parameter file on whitespace (including tabs and line breaks), and lets an argument be
42+
* quoted so that it keeps the whitespace inside it unchanged.
43+
*
44+
* <p>The regex matches either a quoted string (single or double quotes are allowed) or a plain unquoted string.
45+
* Double quotes may appear inside a single-quoted string and vice versa, and are then kept as they are. For
46+
* simplicity, escaped quotes are not handled.
47+
*/
48+
private static final Pattern ARG_MATCHER = Pattern.compile(
49+
"\"([^\"]*)(?:\"|$)" // group 1: string in double quotes (or until EOF), with whitespace allowed
50+
+ "|" // OR
51+
+ "'([^']*)(?:'|$)" // group 2: string in single quotes (or until EOF), with whitespace allowed
52+
+ "|" // OR
53+
+ "([^\\s\"']+)" // group 3: unquoted string, without whitespace and without any quotes
54+
);
4055

4156
/** Parses {@link CommandLineOptions}. */
4257
@SuppressWarnings("for-rollout:NullAway")
4358
static CommandLineOptions parse(Iterable<String> options) {
4459
CommandLineOptions.Builder optionsBuilder = CommandLineOptions.builder();
4560
List<String> expandedOptions = new ArrayList<>();
46-
expandParamsFiles(options, expandedOptions);
61+
expandParamsFiles(options, expandedOptions, new ArrayDeque<>());
4762
Iterator<String> it = expandedOptions.iterator();
4863
while (it.hasNext()) {
4964
String option = it.next();
@@ -226,7 +241,7 @@ private static Range<Integer> parseRange(String arg) {
226241
* Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading the content of the
227242
* file and appending whitespace-delimited options to {@code arguments}.
228243
*/
229-
private static void expandParamsFiles(Iterable<String> args, List<String> expanded) {
244+
private static void expandParamsFiles(Iterable<String> args, List<String> expanded, Deque<String> paramFilesStack) {
230245
for (String arg : args) {
231246
if (arg.isEmpty()) {
232247
continue;
@@ -236,14 +251,35 @@ private static void expandParamsFiles(Iterable<String> args, List<String> expand
236251
} else if (arg.startsWith("@@")) {
237252
expanded.add(arg.substring(1));
238253
} else {
239-
Path path = Paths.get(arg.substring(1));
240-
try {
241-
String sequence = new String(Files.readAllBytes(path), UTF_8);
242-
expandParamsFiles(ARG_SPLITTER.split(sequence), expanded);
243-
} catch (IOException e) {
244-
throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e);
254+
String filename = arg.substring(1);
255+
if (paramFilesStack.contains(filename)) {
256+
throw new IllegalArgumentException("parameter file was included recursively: " + filename);
257+
}
258+
paramFilesStack.push(filename);
259+
expandParamsFiles(getParamsFromFile(filename), expanded, paramFilesStack);
260+
String finishedFilename = paramFilesStack.pop();
261+
Preconditions.checkState(filename.equals(finishedFilename));
262+
}
263+
}
264+
}
265+
266+
/** Reads the parameters from a file, keeping quoted parameters whole. */
267+
private static List<String> getParamsFromFile(String filename) {
268+
String fileContent;
269+
try {
270+
fileContent = Files.readString(Path.of(filename));
271+
} catch (IOException e) {
272+
throw new UncheckedIOException(filename + ": could not read file: " + e.getMessage(), e);
273+
}
274+
List<String> paramsFromFile = new ArrayList<>();
275+
Matcher m = ARG_MATCHER.matcher(fileContent);
276+
while (m.find()) {
277+
for (int i = 1; i <= m.groupCount(); i++) {
278+
if (m.group(i) != null) { // only one group matches: double quotes, single quotes or unquoted string.
279+
paramsFromFile.add(m.group(i));
245280
}
246281
}
247282
}
283+
return paramsFromFile;
248284
}
249285
}

‎open-java-format/src/test/java/com/palantir/javaformat/java/CommandLineOptionsParserTest.java‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static java.nio.charset.StandardCharsets.UTF_8;
1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1920
import static org.assertj.core.api.Assertions.fail;
2021

2122
import com.google.common.collect.Range;
@@ -176,6 +177,61 @@ public void paramsFile() throws IOException {
176177
assertThat(options.files()).containsExactly("L", "M", "ℕ", "@O", "P", "Q");
177178
}
178179

180+
@Test
181+
public void paramsFileWithNesting() throws IOException {
182+
Path outer = Files.createFile(testFolder.resolve("outer"));
183+
Path exit = Files.createFile(testFolder.resolve("exit"));
184+
Path nested1 = Files.createFile(testFolder.resolve("nested1"));
185+
Path nested2 = Files.createFile(testFolder.resolve("nested2"));
186+
Path nested3 = Files.createFile(testFolder.resolve("nested3"));
187+
188+
String[] args = {"--dry-run", "@" + exit, "L", "@" + outer, "U"};
189+
190+
Files.write(exit, "--set-exit-if-changed".getBytes(UTF_8));
191+
Files.write(outer, ("M\n@" + nested1.toAbsolutePath() + "\nT").getBytes(UTF_8));
192+
Files.write(nested1, ("ℕ\n@" + nested2.toAbsolutePath() + "\nS").getBytes(UTF_8));
193+
Files.write(nested2, ("O\n@" + nested3.toAbsolutePath() + "\nR").getBytes(UTF_8));
194+
Files.write(nested3, "P\n\n \n@@Q\n".getBytes(UTF_8));
195+
196+
CommandLineOptions options = CommandLineOptionsParser.parse(Arrays.asList(args));
197+
assertThat(options.files()).containsExactly("L", "M", "ℕ", "O", "P", "@Q", "R", "S", "T", "U");
198+
}
199+
200+
@Test
201+
public void paramsFileWithRecursion() throws IOException {
202+
Path outer = Files.createFile(testFolder.resolve("outer"));
203+
Path exit = Files.createFile(testFolder.resolve("exit"));
204+
Path nested1 = Files.createFile(testFolder.resolve("nested1"));
205+
Path nested2 = Files.createFile(testFolder.resolve("nested2"));
206+
207+
String[] args = {"--dry-run", "@" + exit, "L", "@" + outer, "U"};
208+
209+
Files.write(exit, "--set-exit-if-changed".getBytes(UTF_8));
210+
Files.write(outer, ("M\n@" + nested1.toAbsolutePath() + "\nT").getBytes(UTF_8));
211+
Files.write(nested1, ("ℕ\n@" + nested2.toAbsolutePath() + "\nS").getBytes(UTF_8));
212+
Files.write(nested2, ("O\n@" + nested1.toAbsolutePath() + "\nR").getBytes(UTF_8));
213+
214+
assertThatThrownBy(() -> CommandLineOptionsParser.parse(Arrays.asList(args)))
215+
.isInstanceOf(IllegalArgumentException.class)
216+
.hasMessageStartingWith("parameter file was included recursively: ");
217+
}
218+
219+
@Test
220+
public void paramsFileWithQuotesAndWhitespaces() throws IOException {
221+
Path outer = Files.createFile(testFolder.resolve("outer with whitespace"));
222+
Path exit = Files.createFile(testFolder.resolve("exit with whitespace"));
223+
Path nested = Files.createFile(testFolder.resolve("nested with whitespace"));
224+
225+
String[] args = {"--dry-run", "@" + exit, "L +w", "@" + outer, "Q +w"};
226+
227+
Files.write(exit, "--set-exit-if-changed 'K +w".getBytes(UTF_8));
228+
Files.write(outer, ("\"'M' +w\"\n\"@" + nested.toAbsolutePath() + "\"\n'\"P\" +w'").getBytes(UTF_8));
229+
Files.write(nested, "\"ℕ +w\"\n\n \n\"@@O +w".getBytes(UTF_8));
230+
231+
CommandLineOptions options = CommandLineOptionsParser.parse(Arrays.asList(args));
232+
assertThat(options.files()).containsExactly("K +w", "L +w", "'M' +w", "ℕ +w", "@O +w", "\"P\" +w", "Q +w");
233+
}
234+
179235
@Test
180236
public void assumeFilename() {
181237
assertThat(CommandLineOptionsParser.parse(Arrays.asList("--assume-filename", "Foo.java"))

0 commit comments

Comments
 (0)