Windows, subprocesses: correct flag escaping impl.
Add correct implementation of flag escaping for
subprocesses.
This is a follow-up to https://github.com/bazelbuild/bazel/pull/7413
Next steps:
- replace WindowsProcesses.quoteCommandLine with
the new logic
See https://github.com/bazelbuild/bazel/issues/7122
Closes #7420.
PiperOrigin-RevId: 233900149
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
index 44006aa..bbad849 100644
--- a/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
@@ -142,4 +142,94 @@
}
}
+ /**
+ * Escape command line arguments for {@code CreateProcessW} on Windows.
+ *
+ * <p>This method implements the same algorithm as the native xx_binary launcher does (see
+ * https://github.com/bazelbuild/bazel/pull/7411).
+ *
+ * <p>A similar algorithm with lots of background information is described here:
+ * https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/
+ */
+ public static String windowsEscapeArg(String s) {
+ if (s.isEmpty()) {
+ return "\"\"";
+ } else {
+ boolean needsEscape = false;
+ for (int i = 0; i < s.length(); ++i) {
+ char c = s.charAt(i);
+ if (c == ' ' || c == '"') {
+ needsEscape = true;
+ break;
+ }
+ }
+ if (!needsEscape) {
+ return s;
+ }
+ }
+
+ StringBuilder result = new StringBuilder();
+ result.append('"');
+ int start = 0;
+ for (int i = 0; i < s.length(); ++i) {
+ char c = s.charAt(i);
+ if (c == '"' || c == '\\') {
+ // Copy the segment since the last special character.
+ if (start >= 0) {
+ result.append(s, start, i);
+ start = -1;
+ }
+
+ // Handle the current special character.
+ if (c == '"') {
+ // This is a quote character. Escape it with a single backslash.
+ result.append("\\\"");
+ } else {
+ // This is a backslash (or the first one in a run of backslashes).
+ // Whether we escape it depends on whether the run ends with a quote.
+ int runLen = 1;
+ int j = i + 1;
+ while (j < s.length() && s.charAt(j) == '\\') {
+ runLen++;
+ j++;
+ }
+ if (j == s.length()) {
+ // The run of backslashes goes to the end.
+ // We have to escape every backslash with another backslash.
+ for (int k = 0; k < runLen * 2; ++k) {
+ result.append('\\');
+ }
+ break;
+ } else if (j < s.length() && s.charAt(j) == '"') {
+ // The run of backslashes is terminated by a quote.
+ // We have to escape every backslash with another backslash, and
+ // escape the quote with one backslash.
+ for (int k = 0; k < runLen * 2; ++k) {
+ result.append('\\');
+ }
+ result.append("\\\"");
+ i += runLen; // 'i' is also increased in the loop iteration step
+ } else {
+ // No quote found. Each backslash counts for itself, they must not be
+ // escaped.
+ for (int k = 0; k < runLen; ++k) {
+ result.append('\\');
+ }
+ i += runLen - 1; // 'i' is also increased in the loop iteration step
+ }
+ }
+ } else {
+ // This is not a special character. Start the segment if necessary.
+ if (start < 0) {
+ start = i;
+ }
+ }
+ }
+ // Save final segment after the last special character.
+ if (start != -1) {
+ result.append(s, start, s.length());
+ }
+ result.append('"');
+ return result.toString();
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java
index 65b2620..7e00ddb 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsProcesses.java
@@ -216,9 +216,8 @@
private static native int nativeGetpid();
- // TODO(laszlocsomor): Audit this method and fix bugs. This method implements Bash quoting
- // semantics but Windows quote semantics are different.
- // More info: http://daviddeley.com/autohotkey/parameters/parameters.htm
+ // TODO(laszlocsomor): Replace this method with ShellUtils.windowsEscapeArg in order to fix
+ // https://github.com/bazelbuild/bazel/issues/7122
public static String quoteCommandLine(List<String> argv) {
StringBuilder result = new StringBuilder();
for (int iArg = 0; iArg < argv.size(); iArg++) {
diff --git a/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java b/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java
index 4ebe538..75433e6 100644
--- a/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/shell/ShellUtilsTest.java
@@ -134,4 +134,56 @@
assertTokenizeFails("-Dfoo='bar", "unterminated quotation");
assertTokenizeFails("-Dfoo=\"b'ar", "unterminated quotation");
}
+
+ private void assertWindowsEscapeArg(String arg, String expected) {
+ assertThat(ShellUtils.windowsEscapeArg(arg)).isEqualTo(expected);
+ }
+
+ @Test
+ public void testEscapeCreateProcessArg() {
+ assertWindowsEscapeArg("", "\"\"");
+ assertWindowsEscapeArg(" ", "\" \"");
+ assertWindowsEscapeArg("\"", "\"\\\"\"");
+ assertWindowsEscapeArg("\"\\", "\"\\\"\\\\\"");
+ assertWindowsEscapeArg("\\", "\\");
+ assertWindowsEscapeArg("\\\"", "\"\\\\\\\"\"");
+ assertWindowsEscapeArg("with space", "\"with space\"");
+ assertWindowsEscapeArg("with^caret", "with^caret");
+ assertWindowsEscapeArg("space ^caret", "\"space ^caret\"");
+ assertWindowsEscapeArg("caret^ space", "\"caret^ space\"");
+ assertWindowsEscapeArg("with\"quote", "\"with\\\"quote\"");
+ assertWindowsEscapeArg("with\\backslash", "with\\backslash");
+ assertWindowsEscapeArg("one\\ backslash and \\space", "\"one\\ backslash and \\space\"");
+ assertWindowsEscapeArg("two\\\\backslashes", "two\\\\backslashes");
+ assertWindowsEscapeArg(
+ "two\\\\ backslashes \\\\and space", "\"two\\\\ backslashes \\\\and space\"");
+ assertWindowsEscapeArg("one\\\"x", "\"one\\\\\\\"x\"");
+ assertWindowsEscapeArg("two\\\\\"x", "\"two\\\\\\\\\\\"x\"");
+ assertWindowsEscapeArg("a \\ b", "\"a \\ b\"");
+ assertWindowsEscapeArg("a \\\" b", "\"a \\\\\\\" b\"");
+ assertWindowsEscapeArg("A", "A");
+ assertWindowsEscapeArg("\"a\"", "\"\\\"a\\\"\"");
+ assertWindowsEscapeArg("B C", "\"B C\"");
+ assertWindowsEscapeArg("\"b c\"", "\"\\\"b c\\\"\"");
+ assertWindowsEscapeArg("D\"E", "\"D\\\"E\"");
+ assertWindowsEscapeArg("\"d\"e\"", "\"\\\"d\\\"e\\\"\"");
+ assertWindowsEscapeArg("C:\\F G", "\"C:\\F G\"");
+ assertWindowsEscapeArg("\"C:\\f g\"", "\"\\\"C:\\f g\\\"\"");
+ assertWindowsEscapeArg("C:\\H\"I", "\"C:\\H\\\"I\"");
+ assertWindowsEscapeArg("\"C:\\h\"i\"", "\"\\\"C:\\h\\\"i\\\"\"");
+ assertWindowsEscapeArg("C:\\J\\\"K", "\"C:\\J\\\\\\\"K\"");
+ assertWindowsEscapeArg("\"C:\\j\\\"k\"", "\"\\\"C:\\j\\\\\\\"k\\\"\"");
+ assertWindowsEscapeArg("C:\\L M ", "\"C:\\L M \"");
+ assertWindowsEscapeArg("\"C:\\l m \"", "\"\\\"C:\\l m \\\"\"");
+ assertWindowsEscapeArg("C:\\N O\\", "\"C:\\N O\\\\\"");
+ assertWindowsEscapeArg("\"C:\\n o\\\"", "\"\\\"C:\\n o\\\\\\\"\"");
+ assertWindowsEscapeArg("C:\\P Q\\ ", "\"C:\\P Q\\ \"");
+ assertWindowsEscapeArg("\"C:\\p q\\ \"", "\"\\\"C:\\p q\\ \\\"\"");
+ assertWindowsEscapeArg("C:\\R\\S\\", "C:\\R\\S\\");
+ assertWindowsEscapeArg("C:\\R x\\S\\", "\"C:\\R x\\S\\\\\"");
+ assertWindowsEscapeArg("\"C:\\r\\s\\\"", "\"\\\"C:\\r\\s\\\\\\\"\"");
+ assertWindowsEscapeArg("\"C:\\r x\\s\\\"", "\"\\\"C:\\r x\\s\\\\\\\"\"");
+ assertWindowsEscapeArg("C:\\T U\\W\\", "\"C:\\T U\\W\\\\\"");
+ assertWindowsEscapeArg("\"C:\\t u\\w\\\"", "\"\\\"C:\\t u\\w\\\\\\\"\"");
+ }
}