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");
+  }
+}