Fix external issue 5052 - JSONParser.parse exceptions with some unicode characters

Review at http://gwt-code-reviews.appspot.com/659801

Review by: jat@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8329 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/core/client/JsonUtils.java b/user/src/com/google/gwt/core/client/JsonUtils.java
index c205ece..4fd9942 100644
--- a/user/src/com/google/gwt/core/client/JsonUtils.java
+++ b/user/src/com/google/gwt/core/client/JsonUtils.java
@@ -22,38 +22,117 @@
   @SuppressWarnings("unused")
   private static JavaScriptObject escapeTable = initEscapeTable();
 
+  @SuppressWarnings("unused")
+  private static final boolean hasJsonParse = hasJsonParse();
+
+  /**
+   * Escapes characters within a JSON string than cannot be passed directly to
+   * eval(). Control characters, quotes and backslashes are not affected.
+   */
+  public static native String escapeJsonForEval(String toEscape) /*-{
+    var s = toEscape.replace(/[\xad\u0600-\u0603\u06dd\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]/g, function(x) {
+      return @com.google.gwt.core.client.JsonUtils::escapeChar(Ljava/lang/String;)(x);
+    });
+    return s;
+  }-*/;
+
   /**
    * Returns a quoted, escaped JSON String.
    */
   public static native String escapeValue(String toEscape) /*-{
-    var s = toEscape.replace(/[\x00-\x1F\u2028\u2029"\\]/g, function(x) {
+    var s = toEscape.replace(/[\x00-\x1f\xad\u0600-\u0603\u06dd\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb"\\]/g, function(x) {
       return @com.google.gwt.core.client.JsonUtils::escapeChar(Ljava/lang/String;)(x);
     });
     return "\"" + s + "\"";
   }-*/;
 
-  /*
-   * TODO: Implement safeEval using a proper parser.
-   */
-
   /**
-   * Evaluates a JSON expression. This method does not validate the JSON text
-   * and should only be used on JSON from trusted sources.
+   * Evaluates a JSON expression safely. The payload must evaluate to an Object
+   * or an Array (not a primitive or a String).
+   * 
+   * @param <T> The type of JavaScriptObject that should be returned
+   * @param json The source JSON text
+   * @return The evaluated object
+   * 
+   * @throws IllegalArgumentException if the input is not valid JSON
+   */
+  public static native <T extends JavaScriptObject> T safeEval(String json) /*-{
+    var v;
+    if (@com.google.gwt.core.client.JsonUtils::hasJsonParse) {
+      try {
+        return JSON.parse(json);
+      } catch (e) {
+        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e);
+      }
+    } else {
+      if (!@com.google.gwt.core.client.JsonUtils::safeToEval(Ljava/lang/String;)(json)) {
+        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Illegal character in JSON string");
+      }
+      json = @com.google.gwt.core.client.JsonUtils::escapeJsonForEval(Ljava/lang/String;)(json);
+      try {
+        return eval('(' + json + ')');
+      } catch (e) {
+        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e);
+      }
+    }
+  }-*/;
+  
+  /**
+   * Returns true if the given JSON string may be safely evaluated by {@code
+   * eval()} without undersired side effects or security risks. Note that a true
+   * result from this method does not guarantee that the input string is valid
+   * JSON.  This method does not consider the contents of quoted strings; it
+   * may still be necessary to perform escaping prior to evaluation for correct
+   * results.
+   * 
+   * <p> The technique used is taken from <a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>.
+   */
+  public static native boolean safeToEval(String text) /*-{
+    // Remove quoted strings and disallow anything except:
+    //
+    // 1) symbols and brackets ,:{}[]
+    // 2) numbers: digits 0-9, ., -, +, e, and E
+    // 3) literal values: 'null', 'true' and 'false' = [aeflnr-u]
+    // 4) whitespace: ' ', '\n', '\r', and '\t'
+    return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(text.replace(/"(\\.|[^"\\])*"/g, '')));
+  }-*/;
+  
+  /**
+   * Evaluates a JSON expression using {@code eval()}. This method does not
+   * validate the JSON text and should only be used on JSON from trusted
+   * sources. The payload must evaluate to an Object or an Array (not a
+   * primitive or a String).
    * 
    * @param <T> The type of JavaScriptObject that should be returned
    * @param json The source JSON text
    * @return The evaluated object
    */
   public static native <T extends JavaScriptObject> T unsafeEval(String json) /*-{
-    return eval('(' + json + ')');
+    var escaped = @com.google.gwt.core.client.JsonUtils::escapeJsonForEval(Ljava/lang/String;)(json);
+    try {
+      return eval('(' + escaped + ')');
+    } catch (e) {
+      return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e);
+    }
   }-*/;
 
+  static void throwIllegalArgumentException(String message) {
+    throw new IllegalArgumentException(message);
+  }
+
   @SuppressWarnings("unused")
   private static native String escapeChar(String c) /*-{
     var lookedUp = @com.google.gwt.core.client.JsonUtils::escapeTable[c.charCodeAt(0)];
     return (lookedUp == null) ? c : lookedUp;
   }-*/;
 
+  /**
+   * Returns true if the JSON.parse function is present, false otherwise.
+   */
+  private static native boolean hasJsonParse() /*-{
+    return typeof JSON == "object" && typeof JSON.parse == "function";
+  }-*/;
+
   private static native JavaScriptObject initEscapeTable() /*-{
     var out = [
       "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005",
@@ -64,10 +143,40 @@
       "\\u001E", "\\u001F"];
     out[34] = '\\"';
     out[92] = '\\\\';
-
-    // Unicode line separator chars
-    out[0x2028] = '\\u2028';
-    out[0x2029] = '\\u2029';
+    out[0xad] = '\\u00ad'; // Soft hyphen
+    out[0x600] = '\\u0600'; // Arabic number sign
+    out[0x601] = '\\u0601'; // Arabic sign sanah
+    out[0x602] = '\\u0602'; // Arabic footnote marker
+    out[0x603] = '\\u0603'; // Arabic sign safha
+    out[0x6dd] = '\\u06dd'; // Arabic and of ayah
+    out[0x70f] = '\\u070f'; // Syriac abbreviation mark
+    out[0x17b4] = '\\u17b4'; // Khmer vowel inherent aq
+    out[0x17b5] = '\\u17b5'; // Khmer vowel inherent aa
+    out[0x200c] = '\\u200c'; // Zero width non-joiner
+    out[0x200d] = '\\u200d'; // Zero width joiner
+    out[0x200e] = '\\u200e'; // Left-to-right mark
+    out[0x200f] = '\\u200f'; // Right-to-left mark
+    out[0x2028] = '\\u2028'; // Line separator
+    out[0x2029] = '\\u2029'; // Paragraph separator
+    out[0x202a] = '\\u202a'; // Left-to-right embedding
+    out[0x202b] = '\\u202b'; // Right-to-left embedding
+    out[0x202c] = '\\u202c'; // Pop directional formatting
+    out[0x202d] = '\\u202d'; // Left-to-right override
+    out[0x202e] = '\\u202e'; // Right-to-left override
+    out[0x2060] = '\\u2060'; // Word joiner
+    out[0x2061] = '\\u2061'; // Function application
+    out[0x2062] = '\\u2062'; // Invisible times
+    out[0x2063] = '\\u2063'; // Invisible separator
+    out[0x206a] = '\\u206a'; // Inhibit symmetric swapping
+    out[0x206b] = '\\u206b'; // Activate symmetric swapping
+    out[0x206c] = '\\u206c'; // Inherent Arabic form shaping
+    out[0x206d] = '\\u206d'; // Activate Arabic form shaping
+    out[0x206e] = '\\u206e'; // National digit shapes
+    out[0x206f] = '\\u206f'; // Nominal digit shapes
+    out[0xfeff] = '\\ufeff'; // Zero width no-break space
+    out[0xfff9] = '\\ufff9'; // Intralinear annotation anchor
+    out[0xfffa] = '\\ufffa'; // Intralinear annotation separator
+    out[0xfffb] = '\\ufffb'; // Intralinear annotation terminator
     return out;
   }-*/;
 
diff --git a/user/src/com/google/gwt/json/client/JSONParser.java b/user/src/com/google/gwt/json/client/JSONParser.java
index 7b5ccd9..20320dd 100644
--- a/user/src/com/google/gwt/json/client/JSONParser.java
+++ b/user/src/com/google/gwt/json/client/JSONParser.java
@@ -30,9 +30,35 @@
 
   /**
    * Evaluates a trusted JSON string and returns its JSONValue representation.
-   * CAUTION! For efficiency, this method is implemented using the JavaScript
-   * <code>eval()</code> function, which can execute arbitrary script. DO NOT
-   * pass an untrusted string into this method.
+   * CAUTION! This method calls the JavaScript <code>eval()</code> function,
+   * which can execute arbitrary script. DO NOT pass an untrusted string into
+   * this method.
+   * 
+   * <p>
+   * This method has been deprecated. Please call either
+   * {@link #parseStrict(String)} (for inputs that strictly follow the JSON
+   * specification) or {@link #parseLenient(String)}. The implementation of this
+   * method calls parseLenient.
+   * 
+   * @param jsonString a JSON object to parse
+   * @return a JSONValue that has been built by parsing the JSON string
+   * @throws NullPointerException if <code>jsonString</code> is
+   *           <code>null</code>
+   * @throws IllegalArgumentException if <code>jsonString</code> is empty
+   * 
+   * @deprecated use {@link #parseStrict(String)} or
+   *             {@link #parseLenient(String)}
+   */
+  @Deprecated
+  public static JSONValue parse(String jsonString) {
+    return parseLenient(jsonString);
+  }
+
+  /**
+   * Evaluates a trusted JSON string and returns its JSONValue representation.
+   * CAUTION! This method calls the JavaScript {@code eval()} function, which
+   * can execute arbitrary script. DO NOT pass an untrusted string into this
+   * method.
    * 
    * @param jsonString a JSON object to parse
    * @return a JSONValue that has been built by parsing the JSON string
@@ -40,18 +66,29 @@
    *           <code>null</code>
    * @throws IllegalArgumentException if <code>jsonString</code> is empty
    */
-  public static JSONValue parse(String jsonString) {
-    if (jsonString == null) {
-      throw new NullPointerException();
-    }
-    if (jsonString.length() == 0) {
-      throw new IllegalArgumentException("empty argument");
-    }
-    try {
-      return evaluate(jsonString);
-    } catch (JavaScriptException ex) {
-      throw new JSONException(ex);
-    }
+  public static JSONValue parseLenient(String jsonString) {
+    return parse(jsonString, false);
+  }
+
+  /**
+   * Evaluates a JSON string and returns its JSONValue representation. Where
+   * possible, the browser's {@code JSON.parse function} is used. For older
+   * browsers including IE6 and IE7 that lack a {@code JSON.parse} function, the
+   * input is validated as described in RFC 4627 for safety and passed to
+   * {@code eval()}.
+   * 
+   * @param jsonString a JSON object to parse
+   * @return a JSONValue that has been built by parsing the JSON string
+   * @throws NullPointerException if <code>jsonString</code> is
+   *           <code>null</code>
+   * @throws IllegalArgumentException if <code>jsonString</code> is empty
+   */
+  public static JSONValue parseStrict(String jsonString) {
+    return parse(jsonString, true);
+  }
+  
+  static void throwJSONException(String message) {
+    throw new JSONException(message);
   }
 
   static void throwUnknownTypeException(String typeString) {
@@ -112,8 +149,8 @@
   }
 
   /**
-   * Called from {@link #initTypeMap()}. This method returns a
-   * <code>null</code> pointer, representing JavaScript <code>undefined</code>.
+   * Called from {@link #initTypeMap()}. This method returns a <code>null</code>
+   * pointer, representing JavaScript <code>undefined</code>.
    */
   @SuppressWarnings("unused")
   private static JSONValue createUndefined() {
@@ -122,9 +159,39 @@
 
   /**
    * This method converts <code>jsonString</code> into a JSONValue.
+   * In strict mode (strict == true), one of two code paths is taken:
+   * 1) Call JSON.parse if available, or
+   * 2) Validate the input and call eval()
+   * 
+   * In lenient mode (strict == false), eval() is called without validation.
+   * 
+   * @param strict if true, parse in strict mode. 
    */
-  private static native JSONValue evaluate(String jsonString) /*-{
-    var v = eval('(' + jsonString + ')');
+  private static native JSONValue evaluate(String json, boolean strict) /*-{
+    // Note: we cannot simply call JsonUtils.unsafeEval because it is unable
+    // to return a result for inputs whose outermost type is 'string' in
+    // dev mode.
+    var v;
+    if (strict && @com.google.gwt.core.client.JsonUtils::hasJsonParse) {
+      try {
+        v = JSON.parse(json);
+      } catch (e) {
+        return @com.google.gwt.json.client.JSONParser::throwJSONException(Ljava/lang/String;)("Error parsing JSON: " + e);
+      }
+    } else {
+      if (strict) {
+        // Validate the input according to RFC 4627.
+        if (!@com.google.gwt.core.client.JsonUtils::safeToEval(Ljava/lang/String;)(json)) {
+          return @com.google.gwt.json.client.JSONParser::throwJSONException(Ljava/lang/String;)("Illegal character in JSON string");
+        }
+      }
+      json = @com.google.gwt.core.client.JsonUtils::escapeJsonForEval(Ljava/lang/String;)(json);
+      try {
+        v = eval('(' + json + ')');
+      } catch (e) { 
+        return @com.google.gwt.json.client.JSONParser::throwJSONException(Ljava/lang/String;)("Error parsing JSON: " + e);
+      }
+    }
     var func = @com.google.gwt.json.client.JSONParser::typeMap[typeof v];
     return func ? func(v) : @com.google.gwt.json.client.JSONParser::throwUnknownTypeException(Ljava/lang/String;)(typeof v);
   }-*/;
@@ -140,6 +207,20 @@
     }
   }-*/;
 
+  private static JSONValue parse(String jsonString, boolean strict) {
+    if (jsonString == null) {
+      throw new NullPointerException();
+    }
+    if (jsonString.length() == 0) {
+      throw new IllegalArgumentException("empty argument");
+    }
+    try {
+      return evaluate(jsonString, strict);
+    } catch (JavaScriptException ex) {
+      throw new JSONException(ex);
+    }
+  }
+
   /**
    * Not instantiable.
    */
diff --git a/user/test/com/google/gwt/json/client/JSONTest.java b/user/test/com/google/gwt/json/client/JSONTest.java
index ba639cd..340c362 100644
--- a/user/test/com/google/gwt/json/client/JSONTest.java
+++ b/user/test/com/google/gwt/json/client/JSONTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.json.client;
 
+import com.google.gwt.core.client.JsonUtils;
 import com.google.gwt.junit.client.GWTTestCase;
 
 import java.util.Set;
@@ -140,7 +141,7 @@
       arr.set(i, new JSONNumber(i));
     }
     String s = arr.toString();
-    JSONValue v = JSONParser.parse(s);
+    JSONValue v = parseStrictVsLenient(s);
     JSONArray array = v.isArray();
     assertTrue("v must be an array", array != null);
     assertEquals("Array size must be 10", 10, array.size());
@@ -154,17 +155,17 @@
     assertTrue(JSONBoolean.getInstance(true).booleanValue());
     assertFalse(JSONBoolean.getInstance(false).booleanValue());
 
-    JSONValue trueVal = JSONParser.parse("true");
+    JSONValue trueVal = parseStrictVsLenient("true");
     assertEquals(trueVal, trueVal.isBoolean());
     assertTrue(trueVal.isBoolean().booleanValue());
 
-    JSONValue falseVal = JSONParser.parse("false");
+    JSONValue falseVal = parseStrictVsLenient("false");
     assertEquals(falseVal, falseVal.isBoolean());
     assertFalse(falseVal.isBoolean().booleanValue());
   }
 
   public void testEquals() {
-    JSONArray array = JSONParser.parse("[]").isArray();
+    JSONArray array = parseStrictVsLenient("[]").isArray();
     assertEquals(array, new JSONArray(array.getJavaScriptObject()));
 
     assertEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
@@ -174,33 +175,11 @@
 
     assertEquals(new JSONNumber(3.1), new JSONNumber(3.1));
 
-    JSONObject object = JSONParser.parse("{}").isObject();
+    JSONObject object = parseStrictVsLenient("{}").isObject();
     assertEquals(object, new JSONObject(object.getJavaScriptObject()));
 
     assertEquals(new JSONString("foo"), new JSONString("foo"));
   }
-  
-  public void testHashCode() {
-    JSONArray array = JSONParser.parse("[]").isArray();
-    assertHashCodeEquals(array, new JSONArray(array.getJavaScriptObject()));
-
-    assertHashCodeEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
-    assertHashCodeEquals(JSONBoolean.getInstance(true), JSONBoolean.getInstance(true));
-
-    assertHashCodeEquals(JSONNull.getInstance(), JSONNull.getInstance());
-
-    assertHashCodeEquals(new JSONNumber(3.1), new JSONNumber(3.1));
-
-    JSONObject object = JSONParser.parse("{}").isObject();
-    assertHashCodeEquals(object, new JSONObject(object.getJavaScriptObject()));
-
-    assertHashCodeEquals(new JSONString("foo"), new JSONString("foo"));
-  }
-
-  private void assertHashCodeEquals(Object expected, Object actual) {
-    assertEquals("hashCodes are not equal", expected.hashCode(),
-        actual.hashCode());
-  }
 
   // Null characters do not work in hosted mode
   public void testEscaping() {
@@ -238,6 +217,23 @@
         + "\"c39\":\"/\", \"c40\":\"\\u2028\", \"c41\":\"\\u2029\"}", o.toString());
   }
 
+  public void testHashCode() {
+    JSONArray array = parseStrictVsLenient("[]").isArray();
+    assertHashCodeEquals(array, new JSONArray(array.getJavaScriptObject()));
+
+    assertHashCodeEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
+    assertHashCodeEquals(JSONBoolean.getInstance(true), JSONBoolean.getInstance(true));
+
+    assertHashCodeEquals(JSONNull.getInstance(), JSONNull.getInstance());
+
+    assertHashCodeEquals(new JSONNumber(3.1), new JSONNumber(3.1));
+
+    JSONObject object = parseStrictVsLenient("{}").isObject();
+    assertHashCodeEquals(object, new JSONObject(object.getJavaScriptObject()));
+
+    assertHashCodeEquals(new JSONString("foo"), new JSONString("foo"));
+  }
+
   public void testLargeArrays() {
     JSONArray arr = null;
     for (int j = 1; j < 500; j *= 2) {
@@ -246,8 +242,33 @@
     }
   }
 
+  public void testLenientAndStrict() {
+    String jsonString = "{ a:27, 'b': 'value' }";
+    
+    // parseLenient should succeed
+    JSONValue value = JSONParser.parseLenient(jsonString);
+    JSONObject object = value.isObject();
+    assertEquals(27.0, object.get("a").isNumber().doubleValue());
+    assertEquals("value", object.get("b").isString().stringValue());
+    
+    // parseStrict should fail
+    try {
+      parseStrictVsLenient(jsonString);
+      fail();
+    } catch (JSONException e) {
+    }
+    
+    // Must fail even on browsers that lack JSON.parse()
+    jsonString = "function f() { return 5; }";
+    try {
+      parseStrictVsLenient(jsonString);
+      fail();
+    } catch (JSONException e) {
+    }
+  }
+  
   public void testMenu() {
-    JSONObject v = (JSONObject) JSONParser.parse(menuTest);
+    JSONObject v = (JSONObject) parseStrictVsLenient(menuTest);
     assertTrue(v.containsKey("menu"));
     JSONObject menu = ((JSONObject) v.get("menu"));
     assertEquals(3, menu.keySet().size());
@@ -257,7 +278,7 @@
     JSONObject obj = new JSONObject();
     nestedAux(obj, 3);
     String s1 = obj.toString();
-    String s2 = JSONParser.parse(s1).toString();
+    String s2 = parseStrictVsLenient(s1).toString();
     assertEquals(s1, s2);
     assertEquals(
         "{\"string3\":\"s3\", \"Number3\":3.1, \"Boolean3\":false, "
@@ -331,7 +352,7 @@
       obj.put("Object " + i, new JSONNumber(i));
     }
     String s = obj.toString();
-    JSONValue v = JSONParser.parse(s);
+    JSONValue v = parseStrictVsLenient(s);
     JSONObject objIn = v.isObject();
     assertTrue("v must be an object", objIn != null);
     assertEquals("Object size must be 10", 10, objIn.keySet().size());
@@ -351,27 +372,27 @@
     } catch (NullPointerException t) {
     }
     try {
-      JSONParser.parse(null);
+      parseStrictVsLenient(null);
       fail();
     } catch (NullPointerException t) {
     }
     try {
-      JSONParser.parse("");
+      parseStrictVsLenient("");
       fail();
     } catch (IllegalArgumentException t) {
     }
     try {
-      JSONParser.parse("{\"menu\": {\n" + "  \"id\": \"file\",\n");
+      parseStrictVsLenient("{\"menu\": {\n" + "  \"id\": \"file\",\n");
       fail();
     } catch (JSONException e) {
     }
     assertEquals("\"null\" should be null JSONValue", JSONNull.getInstance(),
-        JSONParser.parse("null"));
+        parseStrictVsLenient("null"));
     assertEquals("5 should be JSONNumber 5", 5d,
-        JSONParser.parse("5").isNumber().doubleValue(), 0.001);
+        parseStrictVsLenient("5").isNumber().doubleValue(), 0.001);
     assertEquals("\"null\" should be null JSONValue", JSONNull.getInstance(),
-        JSONParser.parse("null"));
-    JSONValue somethingHello = JSONParser.parse("[{\"something\":\"hello\"}]");
+        parseStrictVsLenient("null"));
+    JSONValue somethingHello = parseStrictVsLenient("[{\"something\":\"hello\"}]");
     JSONArray somethingHelloArray = somethingHello.isArray();
     assertTrue("somethingHello must be a JSONArray",
         somethingHelloArray != null);
@@ -393,11 +414,108 @@
 
     String toString = obj.toString();
     assertEquals("{\"a\":42, \"\\\\\":43.5, \"\\\"\":44}", toString.trim());
-    JSONValue parseResponse = JSONParser.parse(toString);
+    JSONValue parseResponse = parseStrictVsLenient(toString);
     JSONObject obj2 = parseResponse.isObject();
     assertJSONObjectEquals(obj, obj2);
   }
 
+  // Break up long test into smaller chunks
+  public void testParseUnescaped0() {
+    for (int i = 0x20; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped1() {
+    for (int i = 0x20 + 1; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped2() {
+    for (int i = 0x20 + 2; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped3() {
+    for (int i = 0x20 + 3; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped4() {
+    for (int i = 0x20 + 4; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped5() {
+    for (int i = 0x20 + 5; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped6() {
+    for (int i = 0x20 + 6; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped7() {
+    for (int i = 0x20 + 7; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped8() {
+    for (int i = 0x20 + 8; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescaped9() {
+    for (int i = 0x20 + 9; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescapeda() {
+    for (int i = 0x20 + 10; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescapedb() {
+    for (int i = 0x20 + 11; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescapedc() {
+    for (int i = 0x20 + 12; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescapedd() {
+    for (int i = 0x20 + 13; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescapede() {
+    for (int i = 0x20 + 14; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
+  public void testParseUnescapedf() {
+    for (int i = 0x20 + 15; i <= 0xffff; i += 16) {
+      doTestParseUnescaped(i);
+    }
+  }
+
   public void testSimpleNested() {
     JSONObject j1 = new JSONObject();
     j1.put("test1", new JSONString(""));
@@ -433,7 +551,7 @@
   }
 
   public void testStringTypes() {
-    JSONObject object = JSONParser.parse("{\"a\":\"b\",\"null\":\"foo\"}").isObject();
+    JSONObject object = parseStrictVsLenient("{\"a\":\"b\",\"null\":\"foo\"}").isObject();
     assertNotNull(object);
 
     assertEquals("b",
@@ -452,7 +570,7 @@
   }
 
   public void testUndefined() {
-    JSONObject o = JSONParser.parse("{foo:'foo',bar:null}").isObject();
+    JSONObject o = parseStrictVsLenient("{\"foo\":\"foo\",\"bar\":null}").isObject();
     assertEquals(new JSONString("foo"), o.get("foo"));
     assertEquals(JSONNull.getInstance(), o.get("bar"));
     assertNull(o.get("baz"));
@@ -462,7 +580,7 @@
     o.put("foo", null);
     assertNull(o.get("foo"));
 
-    JSONArray array = JSONParser.parse("['foo',null]").isArray();
+    JSONArray array = parseStrictVsLenient("[\"foo\",null]").isArray();
     assertEquals(new JSONString("foo"), array.get(0));
     assertEquals(JSONNull.getInstance(), array.get(1));
     assertNull(array.get(2));
@@ -473,8 +591,61 @@
     assertNull(array.get(0));
   }
 
+  public void testUnicodeSeparators() {
+    /*
+     * ECMAScript 5 allows unescaped U+2028 (line separator) and U+2029
+     * (paragraph separator) characters to occur inside strings.
+     */
+    String jsonString = "{ \"name\": \"miles\u2028da\u2029vis\", \"ins\u2028tru\u2029ment\": \"trumpet\" }";
+    try {
+      JSONValue parsed = parseStrictVsLenient(jsonString);
+      JSONObject result = parsed.isObject();
+      assertNotNull(result);
+
+      JSONValue nameValue = result.get("name");
+      assertNotNull(nameValue);
+      JSONString nameJsonString = nameValue.isString();
+      assertNotNull(nameJsonString);
+      String nameString = nameJsonString.stringValue();
+      assertEquals("miles\u2028da\u2029vis", nameString);
+      String nameStringQuoted = nameJsonString.toString();
+      assertEquals("\"miles\\u2028da\\u2029vis\"", nameStringQuoted);
+
+      JSONValue instrumentValue = result.get("ins\u2028tru\u2029ment");
+      assertNotNull(instrumentValue);
+      JSONValue instrumentValue2 = result.get("instrument");
+      assertNull(instrumentValue2); // check no name collision
+      JSONString instrumentJsonString = instrumentValue.isString();
+      assertNotNull(instrumentJsonString);
+      String instrumentString = instrumentJsonString.stringValue();
+      assertEquals("trumpet", instrumentString);
+    } catch (JSONException e) {
+      fail(e.getMessage());
+    }
+
+    // U+2028 and U+2029 should not appear outside a string
+    jsonString = "{ \"name\": \"miles davis\",\u2028\"instrument\": \"trumpet\" }";
+    try {
+      parseStrictVsLenient(jsonString);
+      fail();
+    } catch (JSONException e) {
+    }
+
+    jsonString = "{ \"name\":\u2029\"miles davis\", \"instrument\": \"trumpet\" }";
+    try {
+      parseStrictVsLenient(jsonString);
+      fail();
+    } catch (JSONException e) {
+    }
+  }
+
+  public void testUnsafeEval() {
+    JsonUtils.unsafeEval("{\"name\":\"value\"}");
+    JsonUtils.unsafeEval("[0,1,2,3,4]");
+  }
+  
   public void testWidget() {
-    JSONObject v = (JSONObject) JSONParser.parse(widgetTest);
+    JSONObject v = (JSONObject) parseStrictVsLenient(widgetTest);
     JSONObject widget = (JSONObject) v.get("widget");
     JSONObject window = (JSONObject) widget.get("window");
     JSONValue title = window.get("title");
@@ -483,8 +654,13 @@
     assertNotNull(hOffSet.isNumber());
   }
 
+  private void assertHashCodeEquals(Object expected, Object actual) {
+    assertEquals("hashCodes are not equal", expected.hashCode(),
+        actual.hashCode());
+  }
+
   private void checkRoundTripJsonText(String jsonText, String normaltext) {
-    JSONString parsed = JSONParser.parse(jsonText).isString();
+    JSONString parsed = parseStrictVsLenient(jsonText).isString();
     assertEquals(normaltext, parsed.stringValue());
     assertEquals(jsonText, parsed.toString());
   }
@@ -497,6 +673,36 @@
     return arr;
   }
 
+  private void doTestParseUnescaped(int i) {
+    // Skip surrogate pairs
+    if (i >= 0xD800 && i <= 0xDFFF) {
+      return;
+    }
+    char c = (char) i;
+    if (c == '\"' || c == '\\') {
+      return;
+    }
+
+    String key = "na" + c + "me";
+    String value = "miles" + c + "davis";
+    String jsonString = "{ \"" + key + "\": \"" + value + "\"}";
+    try {
+      JSONValue parsed = parseStrictVsLenient(jsonString);
+      JSONObject result = parsed.isObject();
+      assertNotNull("i = " + i, result);
+      Set<String> keys = result.keySet();
+      assertTrue("i = " + i, keys.contains(key));
+      JSONValue nameValue = result.get(key);
+      assertNotNull("i = " + i, nameValue);
+      JSONString nameJsonString = nameValue.isString();
+      assertNotNull("i = " + i, nameJsonString);
+      String nameString = nameJsonString.stringValue();
+      assertEquals("i = " + i, value, nameString);
+    } catch (JSONException e) {
+      fail("i = " + i + ", e = " + e.getMessage());
+    }
+  }
+
   private void nestedAux(JSONObject obj, int i) {
     JSONArray array = new JSONArray();
     JSONString str = new JSONString("s" + i);
@@ -520,6 +726,14 @@
     obj.put("Array" + i, array);
   }
 
+  // Check that parseLenient produces the same results as parseStrict
+  private JSONValue parseStrictVsLenient(String jsonString) {
+    JSONValue strictValue = JSONParser.parseStrict(jsonString);
+    JSONValue lenientValue = JSONParser.parseLenient(jsonString);
+    assertJSONValueEquals(strictValue, lenientValue);
+    return strictValue;
+  }
+
   private JSONArray populateRecursiveArray(int numElements, int recursion) {
     JSONArray newArray = new JSONArray();
     if (recursion <= 0) {