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) {