GN: Allow escape codes to be embedded in strings via $0xFF

I've wanted to use them when writing assert messages. Might also be
useful in write_file().

BUG=

Review URL: https://codereview.chromium.org/1546393002

Cr-Original-Commit-Position: refs/heads/master@{#367622}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: f59e82f6169caace177e452c06377b3d611ed978
diff --git a/tools/gn/parser.cc b/tools/gn/parser.cc
index 79966b8..d3e2f0f 100644
--- a/tools/gn/parser.cc
+++ b/tools/gn/parser.cc
@@ -62,7 +62,8 @@
     "      escape           = `\\` ( \"$\" | `\"` | char ) .\n"
     "      BracketExpansion = \"{\" ( identifier | ArrayAccess | ScopeAccess "
                               ") \"}\" .\n"
-    "      expansion        = \"$\" ( identifier | BracketExpansion ) .\n"
+    "      Hex              = \"0x\" [0-9A-Fa-f][0-9A-Fa-f]\n"
+    "      expansion        = \"$\" ( identifier | BracketExpansion | Hex ) .\n"
     "      char             = /* any character except \"$\", `\"`, or newline "
                              "*/ .\n"
     "\n"
@@ -74,6 +75,9 @@
     "\n"
     "  All other backslashes represent themselves.\n"
     "\n"
+    "  To insert an arbitrary byte value, use $0xFF. For example, to\n"
+    "  insert a newline character: \"Line one$0x0ALine two\".\n"
+    "\n"
     "Punctuation\n"
     "\n"
     "  The following character sequences represent punctuation:\n"
diff --git a/tools/gn/string_utils.cc b/tools/gn/string_utils.cc
index 5ee12d9..7246f75 100644
--- a/tools/gn/string_utils.cc
+++ b/tools/gn/string_utils.cc
@@ -5,7 +5,9 @@
 #include "tools/gn/string_utils.h"
 
 #include <stddef.h>
+#include <cctype>
 
+#include "base/strings/string_number_conversions.h"
 #include "tools/gn/err.h"
 #include "tools/gn/input_file.h"
 #include "tools/gn/parser.h"
@@ -131,7 +133,7 @@
 
 // Handles string interpolations: $identifier and ${expression}
 //
-// |*i| is the index into |input| of the $. This will be updated to point to
+// |*i| is the index into |input| after the $. This will be updated to point to
 // the last character consumed on success. The token is the original string
 // to blame on failure.
 //
@@ -143,13 +145,7 @@
                                size_t* i,
                                std::string* output,
                                Err* err) {
-  size_t dollars_index = *i;
-  (*i)++;
-  if (*i == size) {
-    *err = ErrInsideStringToken(token, dollars_index, 1, "$ at end of string.",
-        "I was expecting an identifier or {...} after the $.");
-    return false;
-  }
+  size_t dollars_index = *i - 1;
 
   if (input[*i] == '{') {
     // Bracketed expression.
@@ -203,6 +199,40 @@
                                       end_offset, output, err);
 }
 
+// Handles a hex literal: $0xFF
+//
+// |*i| is the index into |input| after the $. This will be updated to point to
+// the last character consumed on success. The token is the original string
+// to blame on failure.
+//
+// On failure, returns false and sets the error. On success, appends the
+// char with the given hex value to |*output|.
+bool AppendHexByte(Scope* scope,
+                   const Token& token,
+                   const char* input, size_t size,
+                   size_t* i,
+                   std::string* output,
+                   Err* err) {
+  size_t dollars_index = *i - 1;
+  // "$0" is already known to exist.
+  if (*i + 3 >= size || input[*i + 1] != 'x' || !std::isxdigit(input[*i + 2]) ||
+      !std::isxdigit(input[*i + 3])) {
+    *err = ErrInsideStringToken(
+        token, dollars_index, *i - dollars_index + 1,
+        "Invalid hex character. Hex values must look like 0xFF.");
+    return false;
+  }
+  int value = 0;
+  if (!base::HexStringToInt(base::StringPiece(&input[*i + 2], 2), &value)) {
+    *err = ErrInsideStringToken(token, dollars_index, *i - dollars_index + 1,
+                                "Could not convert hex value.");
+    return false;
+  }
+  *i += 3;
+  output->push_back(value);
+  return true;
+}
+
 }  // namespace
 
 bool ExpandStringLiteral(Scope* scope,
@@ -235,7 +265,16 @@
       }
       output.push_back(input[i]);
     } else if (input[i] == '$') {
-      if (!AppendStringInterpolation(scope, literal, input, size, &i,
+      i++;
+      if (i == size) {
+        *err = ErrInsideStringToken(literal, i - 1, 1, "$ at end of string.",
+            "I was expecting an identifier, 0xFF, or {...} after the $.");
+        return false;
+      }
+      if (input[i] == '0') {
+        if (!AppendHexByte(scope, literal, input, size, &i, &output, err))
+          return false;
+      } else if (!AppendStringInterpolation(scope, literal, input, size, &i,
                                      &output, err))
         return false;
     } else {
diff --git a/tools/gn/string_utils_unittest.cc b/tools/gn/string_utils_unittest.cc
index 4e18eb5..d62e17e 100644
--- a/tools/gn/string_utils_unittest.cc
+++ b/tools/gn/string_utils_unittest.cc
@@ -69,6 +69,10 @@
   EXPECT_TRUE(CheckExpansionCase("$onescope", "{\n  one = 1\n}", true));
   EXPECT_TRUE(CheckExpansionCase("$onelist", "[1]", true));
 
+  // Hex values
+  EXPECT_TRUE(CheckExpansionCase("$0x0AA", "\x0A""A", true));
+  EXPECT_TRUE(CheckExpansionCase("$0x0a$0xfF", "\x0A\xFF", true));
+
   // Errors
   EXPECT_TRUE(CheckExpansionCase("hello #$", nullptr, false));
   EXPECT_TRUE(CheckExpansionCase("hello #$%", nullptr, false));
@@ -76,6 +80,12 @@
   EXPECT_TRUE(CheckExpansionCase("hello #${}", nullptr, false));
   EXPECT_TRUE(CheckExpansionCase("hello #$nonexistant", nullptr, false));
   EXPECT_TRUE(CheckExpansionCase("hello #${unterminated", nullptr, false));
+  EXPECT_TRUE(CheckExpansionCase("hex truncated: $0", nullptr, false));
+  EXPECT_TRUE(CheckExpansionCase("hex truncated: $0x", nullptr, false));
+  EXPECT_TRUE(CheckExpansionCase("hex truncated: $0x0", nullptr, false));
+  EXPECT_TRUE(CheckExpansionCase("hex with bad char: $0a", nullptr, false));
+  EXPECT_TRUE(CheckExpansionCase("hex with bad char: $0x1z", nullptr, false));
+  EXPECT_TRUE(CheckExpansionCase("hex with bad char: $0xz1", nullptr, false));
 
   // Unknown backslash values aren't special.
   EXPECT_TRUE(CheckExpansionCase("\\", "\\", true));