Fix external issue 4425 - '\0' chars in string literals were being escaped
incorrectly.  This patch fixes that bug and also uses shorter octal escapes
rather than hexadecimal escapes where possible.

Review by: jat



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7358 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
index 78dc37c..56b8200 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
@@ -1266,7 +1266,6 @@
    * one is used less inside the string.
    */
   private void printStringLiteral(String value) {
-
     char[] chars = value.toCharArray();
     final int n = chars.length;
     int quoteCount = 0;
@@ -1298,9 +1297,6 @@
 
       int escape = -1;
       switch (c) {
-        case 0:
-          escape = '0';
-          break;
         case '\b':
           escape = 'b';
           break;
@@ -1332,20 +1328,39 @@
         result.append('\\');
         result.append((char) escape);
       } else {
-        int hexSize;
-        if (c < 256) {
-          // 2-digit hex
-          result.append("\\x");
-          hexSize = 2;
+        /*
+         * Emit characters from 0 to 31 that don't have a single character
+         * escape sequence in octal where possible. This saves one or two
+         * characters compared to the hexadecimal format '\xXX'.
+         * 
+         * These short octal sequences may only be used at the end of the string
+         * or where the following character is a non-digit. Otherwise, the
+         * following character would be incorrectly interpreted as belonging to
+         * the sequence.
+         */
+        if (c < ' ' &&
+            (i == n - 1 || chars[i + 1] < '0' || chars[i + 1] > '9')) {
+          result.append('\\');
+          if (c > 0x7) {
+            result.append((char) ('0' + (0x7 & (c >> 3))));
+          }
+          result.append((char) ('0' + (0x7 & c)));
         } else {
-          // Unicode.
-          result.append("\\u");
-          hexSize = 4;
-        }
-        // append hexadecimal form of ch left-padded with 0
-        for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
-          int digit = 0xf & (c >> shift);
-          result.append(HEX_DIGITS[digit]);
+          int hexSize;
+          if (c < 256) {
+            // 2-digit hex
+            result.append("\\x");
+            hexSize = 2;
+          } else {
+            // Unicode.
+            result.append("\\u");
+            hexSize = 4;
+          }
+          // append hexadecimal form of ch left-padded with 0
+          for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
+            int digit = 0xf & (c >> shift);
+            result.append(HEX_DIGITS[digit]);
+          }
         }
       }
     }
diff --git a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
index 4eaed72..b1a2002 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsToStringGenerationVisitorAccuracyTest.java
@@ -16,8 +16,10 @@
 package com.google.gwt.dev.js;
 
 import com.google.gwt.dev.jjs.SourceOrigin;
+import com.google.gwt.dev.jjs.ast.JStringLiteral;
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsStringLiteral;
 import com.google.gwt.dev.js.ast.JsVisitor;
 import com.google.gwt.dev.util.DefaultTextOutput;
 import com.google.gwt.dev.util.TextOutput;
@@ -140,6 +142,38 @@
     // + prefix stripped when operand is literal number
     doTest("var x = +42","var x = 42");
   }
+  
+  public void testEscapes() throws Exception {
+    // Use short octal escapes at the end of the string or when the next
+    // character is a non-digit
+    doTestEscapes("\u0000", "'\\0'");
+    doTestEscapes("a\u0000", "'a\\0'");
+    doTestEscapes("\u0000a", "'\\0a'");
+    doTestEscapes("a\u0000a", "'a\\0a'");
+    // Ensure hex escapes are used where octal is not possible due to a
+    // following digit
+    doTestEscapes("\u00006", "'\\x006'");
+    doTestEscapes("\u00006\u0000", "'\\x006\\0'");
+
+    // Single-digit octal escapes or special cases (\b,\t,\n\,f\,\r)
+    // for characters from 0 to 15
+    doTestEscapes("\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007",
+        "'\\0\\1\\2\\3\\4\\5\\6\\7'");
+    doTestEscapes("\u0008\u0009\n\u000b\u000c\r\u000e\u000f",
+        "'\\b\\t\\n\\13\\f\\r\\16\\17'");
+    
+    // Use two-digit octal escapes for characters from 16 to 31
+    doTestEscapes("\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017",
+        "'\\20\\21\\22\\23\\24\\25\\26\\27'");
+    doTestEscapes("\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f",
+        "'\\30\\31\\32\\33\\34\\35\\36\\37'");
+    
+    // Use two-digit hex escapes for characters up to 0xff
+    doTestEscapes("\u007f\u00ab", "'\\x7F\\xAB'");
+    
+    // Use four-digit unicode escapes for characters from 0x100 up
+    doTestEscapes("\u0100\u117f\u2345", "'\\u0100\\u117F\\u2345'");
+  }
 
   private void doTest(String js) throws Exception {
     List<JsStatement> expected = JsParser.parse(SourceOrigin.UNKNOWN,
@@ -158,6 +192,12 @@
         new JsProgram().getScope(), new StringReader(expectedJs));
     ComparingVisitor.exec(expected, actual);
   }
+  
+  private void doTestEscapes(String value, String expected) throws Exception {
+    String actual =
+      new JsProgram().getStringLiteral(SourceOrigin.UNKNOWN, value).toString();
+    assertEquals(expected, actual);
+  }
 
   private List<JsStatement> parse(List<JsStatement> expected, boolean compact)
       throws Exception {