Merge pull request #1994 from afq984/1e408562-f801-4644-b703-46cd9c672400

compdb: escape control characters in JSON strings
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bc02c4d..b49c5b0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -103,6 +103,7 @@
 	src/eval_env.cc
 	src/graph.cc
 	src/graphviz.cc
+	src/json.cc
 	src/line_printer.cc
 	src/manifest_parser.cc
 	src/metrics.cc
@@ -196,6 +197,7 @@
     src/dyndep_parser_test.cc
     src/edit_distance_test.cc
     src/graph_test.cc
+    src/json_test.cc
     src/lexer_test.cc
     src/manifest_parser_test.cc
     src/missing_deps_test.cc
diff --git a/configure.py b/configure.py
index ffa75c7..4ca78fb 100755
--- a/configure.py
+++ b/configure.py
@@ -507,6 +507,7 @@
              'eval_env',
              'graph',
              'graphviz',
+             'json',
              'lexer',
              'line_printer',
              'manifest_parser',
@@ -577,6 +578,7 @@
              'disk_interface_test',
              'edit_distance_test',
              'graph_test',
+             'json_test',
              'lexer_test',
              'manifest_parser_test',
              'missing_deps_test',
diff --git a/src/json.cc b/src/json.cc
new file mode 100644
index 0000000..4bbf6e1
--- /dev/null
+++ b/src/json.cc
@@ -0,0 +1,53 @@
+// Copyright 2021 Google Inc. 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.
+
+#include "json.h"
+
+#include <cstdio>
+#include <string>
+
+std::string EncodeJSONString(const std::string& in) {
+  static const char* hex_digits = "0123456789abcdef";
+  std::string out;
+  out.reserve(in.length() * 1.2);
+  for (std::string::const_iterator it = in.begin(); it != in.end(); ++it) {
+    char c = *it;
+    if (c == '\b')
+      out += "\\b";
+    else if (c == '\f')
+      out += "\\f";
+    else if (c == '\n')
+      out += "\\n";
+    else if (c == '\r')
+      out += "\\r";
+    else if (c == '\t')
+      out += "\\t";
+    else if (0x0 <= c && c < 0x20) {
+      out += "\\u00";
+      out += hex_digits[c >> 4];
+      out += hex_digits[c & 0xf];
+    } else if (c == '\\')
+      out += "\\\\";
+    else if (c == '\"')
+      out += "\\\"";
+    else
+      out += c;
+  }
+  return out;
+}
+
+void PrintJSONString(const std::string& in) {
+  std::string out = EncodeJSONString(in);
+  fwrite(out.c_str(), 1, out.length(), stdout);
+}
diff --git a/src/json.h b/src/json.h
new file mode 100644
index 0000000..f39c759
--- /dev/null
+++ b/src/json.h
@@ -0,0 +1,26 @@
+// Copyright 2021 Google Inc. 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.
+
+#ifndef NINJA_JSON_H_
+#define NINJA_JSON_H_
+
+#include <string>
+
+// Encode a string in JSON format without encolsing quotes
+std::string EncodeJSONString(const std::string& in);
+
+// Print a string in JSON format to stdout without enclosing quotes
+void PrintJSONString(const std::string& in);
+
+#endif
diff --git a/src/json_test.cc b/src/json_test.cc
new file mode 100644
index 0000000..b4afc73
--- /dev/null
+++ b/src/json_test.cc
@@ -0,0 +1,40 @@
+// Copyright 2021 Google Inc. 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.
+
+#include "json.h"
+
+#include "test.h"
+
+TEST(JSONTest, RegularAscii) {
+  EXPECT_EQ(EncodeJSONString("foo bar"), "foo bar");
+}
+
+TEST(JSONTest, EscapedChars) {
+  EXPECT_EQ(EncodeJSONString("\"\\\b\f\n\r\t"),
+            "\\\""
+            "\\\\"
+            "\\b\\f\\n\\r\\t");
+}
+
+// codepoints between 0 and 0x1f should be escaped
+TEST(JSONTest, ControlChars) {
+  EXPECT_EQ(EncodeJSONString("\x01\x1f"), "\\u0001\\u001f");
+}
+
+// Leave them alone as JSON accepts unicode literals
+// out of control character range
+TEST(JSONTest, UTF8) {
+  const char* utf8str = "\xe4\xbd\xa0\xe5\xa5\xbd";
+  EXPECT_EQ(EncodeJSONString(utf8str), utf8str);
+}
diff --git a/src/ninja.cc b/src/ninja.cc
index 32cf00e..1d94442 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -41,6 +41,7 @@
 #include "disk_interface.h"
 #include "graph.h"
 #include "graphviz.h"
+#include "json.h"
 #include "manifest_parser.h"
 #include "metrics.h"
 #include "missing_deps.h"
@@ -773,15 +774,6 @@
   return cleaner.CleanDead(build_log_.entries());
 }
 
-void EncodeJSONString(const char *str) {
-  while (*str) {
-    if (*str == '"' || *str == '\\')
-      putchar('\\');
-    putchar(*str);
-    str++;
-  }
-}
-
 enum EvaluateCommandMode {
   ECM_NORMAL,
   ECM_EXPAND_RSPFILE
@@ -814,13 +806,13 @@
 void printCompdb(const char* const directory, const Edge* const edge,
                  const EvaluateCommandMode eval_mode) {
   printf("\n  {\n    \"directory\": \"");
-  EncodeJSONString(directory);
+  PrintJSONString(directory);
   printf("\",\n    \"command\": \"");
-  EncodeJSONString(EvaluateCommandWithRspfile(edge, eval_mode).c_str());
+  PrintJSONString(EvaluateCommandWithRspfile(edge, eval_mode));
   printf("\",\n    \"file\": \"");
-  EncodeJSONString(edge->inputs_[0]->path().c_str());
+  PrintJSONString(edge->inputs_[0]->path());
   printf("\",\n    \"output\": \"");
-  EncodeJSONString(edge->outputs_[0]->path().c_str());
+  PrintJSONString(edge->outputs_[0]->path());
   printf("\"\n  }");
 }