Support gcc/clang param file method of escaping strings
Gcc/clang param files do not support shell escaping, so we need to use
a different method to escape those arguments. Implement such method
and use it for parameter files for compile actions.
RELNOTES: none
PiperOrigin-RevId: 248717716
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java b/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java
index 6a91a28..6a9e61a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java
@@ -16,6 +16,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.unsafe.StringUnsafe;
import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.GccParamFileEscaper;
import com.google.devtools.build.lib.util.ShellEscaper;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
@@ -55,13 +56,18 @@
UNQUOTED,
/**
- * A parameter file where each parameter is correctly quoted for shell
- * use, and separated by white space (space, tab, newline). This format is
- * safe for all characters, but must be specially supported by the tool. In
- * particular, it must not be used with gcc and related tools, which do not
- * support this format as it is.
+ * A parameter file where each parameter is correctly quoted for shell use, and separated by
+ * white space (space, tab, newline). This format is safe for all characters, but must be
+ * specially supported by the tool. In particular, it must not be used with gcc and related
+ * tools, which do not support this format as it is.
*/
- SHELL_QUOTED;
+ SHELL_QUOTED,
+
+ /**
+ * A parameter file where each parameter is correctly quoted for gcc or clang use, and separated
+ * by white space (space, tab, newline).
+ */
+ GCC_QUOTED;
}
@VisibleForTesting
@@ -92,8 +98,10 @@
throws IOException {
switch (type) {
case SHELL_QUOTED:
- Iterable<String> quotedContent = ShellEscaper.escapeAll(arguments);
- writeContent(out, quotedContent, charset);
+ writeContent(out, ShellEscaper.escapeAll(arguments), charset);
+ break;
+ case GCC_QUOTED:
+ writeContent(out, GccParamFileEscaper.escapeAll(arguments), charset);
break;
case UNQUOTED:
writeContent(out, arguments, charset);
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
index fb520b5..3aeff9b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
@@ -1222,7 +1222,8 @@
new ParamFileActionInput(
paramFilePath,
compileCommandLine.getCompilerOptions(overwrittenVariables),
- ParameterFileType.SHELL_QUOTED,
+ // TODO(b/132888308): Support MSVC, which has its own method of escaping strings.
+ ParameterFileType.GCC_QUOTED,
StandardCharsets.ISO_8859_1);
} catch (CommandLineExpansionException e) {
throw new ActionExecutionException(
diff --git a/src/main/java/com/google/devtools/build/lib/util/GccParamFileEscaper.java b/src/main/java/com/google/devtools/build/lib/util/GccParamFileEscaper.java
new file mode 100644
index 0000000..247b32d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/GccParamFileEscaper.java
@@ -0,0 +1,73 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.util;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.escape.CharEscaper;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Utility class to escape strings for use in param files for gcc or clang.
+ *
+ * <p>Gcc and Clang interpret the following characters specially: single quote ('), double quote
+ * ("), backslash (\), space ( ), tab (\t), carriage return (\r), newline (\n), form feed (\f), and
+ * vertical tab (\u000B). All can be escaped by prefixing the symbol with a backslash.
+ */
+@Immutable
+public final class GccParamFileEscaper extends CharEscaper {
+ public static final GccParamFileEscaper INSTANCE = new GccParamFileEscaper();
+
+ private static final Function<String, String> AS_FUNCTION = INSTANCE.asFunction();
+
+ private static final CharMatcher UNSAFECHAR_MATCHER =
+ CharMatcher.anyOf("'\"\\ \t\r\n\f\u000B").precomputed();
+
+ @Override
+ public String escape(String string) {
+ if (string.isEmpty()) {
+ // Empty string is a special case: needs to be quoted to ensure that it
+ // gets treated as a separate argument.
+ return "''";
+ } else {
+ return super.escape(string);
+ }
+ }
+
+ @Override
+ public char[] escape(char c) {
+ if (!UNSAFECHAR_MATCHER.matches(c)) {
+ return null;
+ } else {
+ char[] result = new char[2];
+ result[0] = '\\';
+ result[1] = c;
+ return result;
+ }
+ }
+
+ public static String escapeString(String unescaped) {
+ return INSTANCE.escape(unescaped);
+ }
+
+ /**
+ * Transforms the input {@code Iterable} of unescaped strings to an {@code Iterable} of escaped
+ * ones. The escaping is done lazily.
+ */
+ public static Iterable<String> escapeAll(Iterable<? extends String> unescaped) {
+ return Iterables.transform(unescaped, AS_FUNCTION);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/util/GccParamFileEscaperTest.java b/src/test/java/com/google/devtools/build/lib/util/GccParamFileEscaperTest.java
new file mode 100644
index 0000000..2be435a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/util/GccParamFileEscaperTest.java
@@ -0,0 +1,53 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.util.GccParamFileEscaper.escapeString;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link GccParamFileEscaper}. */
+@RunWith(JUnit4.class)
+public class GccParamFileEscaperTest {
+
+ @Test
+ public void testEscapeString() throws Exception {
+ assertThat(escapeString("")).isEqualTo("''");
+ assertThat(escapeString("foo")).isEqualTo("foo");
+ assertThat(escapeString("'foo'")).isEqualTo("\\'foo\\'");
+ assertThat(escapeString("\"foo\"")).isEqualTo("\\\"foo\\\"");
+ assertThat(escapeString("\\foo")).isEqualTo("\\\\foo");
+ assertThat(escapeString("foo bar")).isEqualTo("foo\\ bar");
+ assertThat(escapeString("foo\tbar")).isEqualTo("foo\\\tbar");
+ assertThat(escapeString("foo\rbar")).isEqualTo("foo\\\rbar");
+ assertThat(escapeString("foo\n'foo'\n")).isEqualTo("foo\\\n\\'foo\\'\\\n");
+ assertThat(escapeString("foo\fbar")).isEqualTo("foo\\\fbar");
+ assertThat(escapeString("foo\u000Bbar")).isEqualTo("foo\\\u000Bbar");
+ assertThat(escapeString("${filename%.c}.o")).isEqualTo("${filename%.c}.o");
+ }
+
+ @Test
+ public void testEscapeAll() throws Exception {
+ Set<String> escaped =
+ ImmutableSet.copyOf(GccParamFileEscaper.escapeAll(Arrays.asList("foo", "'foo'", "foo\n")));
+ assertThat(escaped).containsExactly("foo", "\\'foo\\'", "foo\\\n");
+ }
+}