diff options
author | Miguel de Icaza <miguel@gnome.org> | 2015-03-06 17:47:56 +0300 |
---|---|---|
committer | Miguel de Icaza <miguel@gnome.org> | 2015-03-06 17:47:56 +0300 |
commit | 48992d4b3f8b568be17180372160d2f3e03b8ccb (patch) | |
tree | e871d315a5872dcc69b3c98d4f502e749ae0cfc7 /mcs/class/System.Json | |
parent | 0a968edad3def3e32f4756f3b9e1893b546277d5 (diff) | |
parent | d65e93b74fd5623881e561dd0be240003a7d6c54 (diff) |
Merge pull request #1155 from steffen-kiess/json-string
Fix System.Json string handling and make order of JsonObject entries deterministic
Diffstat (limited to 'mcs/class/System.Json')
4 files changed, 125 insertions, 7 deletions
diff --git a/mcs/class/System.Json/System.Json/JsonObject.cs b/mcs/class/System.Json/System.Json/JsonObject.cs index 33177b1c5f2..91366a2243a 100644 --- a/mcs/class/System.Json/System.Json/JsonObject.cs +++ b/mcs/class/System.Json/System.Json/JsonObject.cs @@ -12,11 +12,12 @@ namespace System.Json { public class JsonObject : JsonValue, IDictionary<string, JsonValue>, ICollection<JsonPair> { - Dictionary<string, JsonValue> map; + // Use SortedDictionary to make result of ToString() deterministic + SortedDictionary<string, JsonValue> map; public JsonObject (params JsonPair [] items) { - map = new Dictionary<string, JsonValue> (); + map = new SortedDictionary<string, JsonValue> (StringComparer.Ordinal); if (items != null) AddRange (items); @@ -27,7 +28,7 @@ namespace System.Json if (items == null) throw new ArgumentNullException ("items"); - map = new Dictionary<string, JsonValue> (); + map = new SortedDictionary<string, JsonValue> (StringComparer.Ordinal); AddRange (items); } diff --git a/mcs/class/System.Json/System.Json/JsonPrimitive.cs b/mcs/class/System.Json/System.Json/JsonPrimitive.cs index 5d47eb4a4e6..24f51ed560a 100644 --- a/mcs/class/System.Json/System.Json/JsonPrimitive.cs +++ b/mcs/class/System.Json/System.Json/JsonPrimitive.cs @@ -161,6 +161,8 @@ namespace System.Json case JsonType.String: if (value is string || value == null) return (string) value; + if (value is char) + return value.ToString (); throw new NotImplementedException ("GetFormattedString from value type " + value.GetType ()); case JsonType.Number: string s; diff --git a/mcs/class/System.Json/System.Json/JsonValue.cs b/mcs/class/System.Json/System.Json/JsonValue.cs index d703edd5512..ac3f723e2ad 100644 --- a/mcs/class/System.Json/System.Json/JsonValue.cs +++ b/mcs/class/System.Json/System.Json/JsonValue.cs @@ -197,13 +197,37 @@ namespace System.Json throw new InvalidOperationException (); } + // Characters which have to be escaped: + // - Required by JSON Spec: Control characters, '"' and '\\' + // - Broken surrogates to make sure the JSON string is valid Unicode + // (and can be encoded as UTF8) + // - JSON does not require U+2028 and U+2029 to be escaped, but + // JavaScript does require this: + // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character/9168133#9168133 + // - '/' also does not have to be escaped, but escaping it when + // preceeded by a '<' avoids problems with JSON in HTML <script> tags + bool NeedEscape (string src, int i) { + char c = src [i]; + return c < 32 || c == '"' || c == '\\' + // Broken lead surrogate + || (c >= '\uD800' && c <= '\uDBFF' && + (i == src.Length - 1 || src [i + 1] < '\uDC00' || src [i + 1] > '\uDFFF')) + // Broken tail surrogate + || (c >= '\uDC00' && c <= '\uDFFF' && + (i == 0 || src [i - 1] < '\uD800' || src [i - 1] > '\uDBFF')) + // To produce valid JavaScript + || c == '\u2028' || c == '\u2029' + // Escape "</" for <script> tags + || (c == '/' && i > 0 && src [i - 1] == '<'); + } + internal string EscapeString (string src) { if (src == null) return null; for (int i = 0; i < src.Length; i++) - if (src [i] == '"' || src [i] == '\\') { + if (NeedEscape (src, i)) { var sb = new StringBuilder (); if (i > 0) sb.Append (src, 0, i); @@ -216,10 +240,22 @@ namespace System.Json { int start = cur; for (int i = cur; i < src.Length; i++) - if (src [i] == '"' || src [i] == '\\') { + if (NeedEscape (src, i)) { sb.Append (src, start, i - start); - sb.Append ('\\'); - sb.Append (src [i]); + switch (src [i]) { + case '\b': sb.Append ("\\b"); break; + case '\f': sb.Append ("\\f"); break; + case '\n': sb.Append ("\\n"); break; + case '\r': sb.Append ("\\r"); break; + case '\t': sb.Append ("\\t"); break; + case '\"': sb.Append ("\\\""); break; + case '\\': sb.Append ("\\\\"); break; + case '/': sb.Append ("\\/"); break; + default: + sb.Append ("\\u"); + sb.Append (((int) src [i]).ToString ("x04")); + break; + } start = i + 1; } sb.Append (src, start, src.Length - start); diff --git a/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs b/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs index 02dd106dfd8..15d5bd82b38 100644 --- a/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs +++ b/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs @@ -46,6 +46,26 @@ namespace MonoTests.System Assert.AreEqual (str, "[1, 2, 3, null]"); } + // Test that we correctly serialize JsonObject with null elements. + [Test] + public void ToStringOnJsonObjectWithNulls () { + var j = JsonValue.Load (new StringReader ("{\"a\":null,\"b\":2}")); + Assert.AreEqual (2, j.Count, "itemcount"); + Assert.AreEqual (JsonType.Object, j.JsonType, "type"); + var str = j.ToString (); + Assert.AreEqual (str, "{\"a\": null, \"b\": 2}"); + } + + [Test] + public void JsonObjectOrder () { + var obj = new JsonObject (); + obj["a"] = 1; + obj["c"] = 3; + obj["b"] = 2; + var str = obj.ToString (); + Assert.AreEqual (str, "{\"a\": 1, \"b\": 2, \"c\": 3}"); + } + [Test] public void QuoteEscapeBug_20869 () { @@ -181,5 +201,64 @@ namespace MonoTests.System Thread.CurrentThread.CurrentCulture = old; } } + + // Convert a string to json and parse the string, then compare the result to the original value + void CheckString (string str) + { + var json = new JsonPrimitive (str).ToString (); + // Check whether the string is valid Unicode (will throw for broken surrogate pairs) + new UTF8Encoding (false, true).GetBytes (json); + string jvalue = (string) JsonValue.Parse (json); + Assert.AreEqual (str, jvalue); + } + + // String handling: http://tools.ietf.org/html/rfc7159#section-7 + [Test] + public void CheckStrings () + { + Assert.AreEqual ("\"test\"", new JsonPrimitive ("test").ToString ()); + // Handling of characters + Assert.AreEqual ("\"f\"", new JsonPrimitive ('f').ToString ()); + Assert.AreEqual ('f', (char) JsonValue.Parse ("\"f\"")); + + // Control characters with special escape sequence + Assert.AreEqual ("\"\\b\\f\\n\\r\\t\"", new JsonPrimitive ("\b\f\n\r\t").ToString ()); + // Other characters which must be escaped + Assert.AreEqual (@"""\""\\""", new JsonPrimitive ("\"\\").ToString ()); + // Control characters without special escape sequence + for (int i = 0; i < 32; i++) + if (i != '\b' && i != '\f' && i != '\n' && i != '\r' && i != '\t') + Assert.AreEqual ("\"\\u" + i.ToString ("x04") + "\"", new JsonPrimitive ("" + (char) i).ToString ()); + + // JSON does not require U+2028 and U+2029 to be escaped, but + // JavaScript does require this: + // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character/9168133#9168133 + Assert.AreEqual ("\"\\u2028\\u2029\"", new JsonPrimitive ("\u2028\u2029").ToString ()); + + // '/' also does not have to be escaped, but escaping it when + // preceeded by a '<' avoids problems with JSON in HTML <script> tags + Assert.AreEqual ("\"<\\/\"", new JsonPrimitive ("</").ToString ()); + // Don't escape '/' in other cases as this makes the JSON hard to read + Assert.AreEqual ("\"/bar\"", new JsonPrimitive ("/bar").ToString ()); + Assert.AreEqual ("\"foo/bar\"", new JsonPrimitive ("foo/bar").ToString ()); + + CheckString ("test\b\f\n\r\t\"\\/</\0x"); + for (int i = 0; i < 65536; i++) + CheckString ("x" + ((char) i)); + + // Check broken surrogate pairs + CheckString ("\ud800"); + CheckString ("x\ud800"); + CheckString ("\udfff\ud800"); + CheckString ("\ude03\ud912"); + CheckString ("\uc000\ubfff"); + CheckString ("\udfffx"); + // Valid strings should not be escaped: + Assert.AreEqual ("\"\ud7ff\"", new JsonPrimitive ("\ud7ff").ToString ()); + Assert.AreEqual ("\"\ue000\"", new JsonPrimitive ("\ue000").ToString ()); + Assert.AreEqual ("\"\ud800\udc00\"", new JsonPrimitive ("\ud800\udc00").ToString ()); + Assert.AreEqual ("\"\ud912\ude03\"", new JsonPrimitive ("\ud912\ude03").ToString ()); + Assert.AreEqual ("\"\udbff\udfff\"", new JsonPrimitive ("\udbff\udfff").ToString ()); + } } } |