diff options
author | Steffen Kieß <Steffen.Kiess@ipvs.uni-stuttgart.de> | 2014-07-07 21:22:29 +0400 |
---|---|---|
committer | Steffen Kieß <Steffen.Kiess@ipvs.uni-stuttgart.de> | 2014-07-07 21:22:29 +0400 |
commit | b005523cb17989ffc0d2f2662ce7200fb49803b1 (patch) | |
tree | 05301182fdb14931488bb702fcf1263488ca33d8 | |
parent | 66b9914768e9b8ffe7c1eee0ae5d95b287b49a6c (diff) |
Fix several issues in System.Json
- Fix detection of extra characters in JSON input
- Fix detection of invalid negative numeric literals
- Disallow leading zeros
- Use "round-trip" format for floating point numbers
- Serialize NaN and Infinity as strings
- Use NumberFormatInfo.InvariantInfo for Convert.To*()
- Use standard number parsing methods in ReadNumericLiteral()
- Add test cases
4 files changed, 213 insertions, 70 deletions
diff --git a/mcs/class/System.Json/System.Json/JsonPrimitive.cs b/mcs/class/System.Json/System.Json/JsonPrimitive.cs index 64518785b87..5d47eb4a4e6 100644 --- a/mcs/class/System.Json/System.Json/JsonPrimitive.cs +++ b/mcs/class/System.Json/System.Json/JsonPrimitive.cs @@ -163,7 +163,16 @@ namespace System.Json return (string) value; throw new NotImplementedException ("GetFormattedString from value type " + value.GetType ()); case JsonType.Number: - return ((IFormattable) value).ToString ("G", NumberFormatInfo.InvariantInfo); + string s; + if (value is float || value is double) + // Use "round-trip" format + s = ((IFormattable) value).ToString ("R", NumberFormatInfo.InvariantInfo); + else + s = ((IFormattable) value).ToString ("G", NumberFormatInfo.InvariantInfo); + if (s == "NaN" || s == "Infinity" || s == "-Infinity") + return "\"" + s + "\""; + else + return s; default: throw new InvalidOperationException (); } diff --git a/mcs/class/System.Json/System.Json/JsonValue.cs b/mcs/class/System.Json/System.Json/JsonValue.cs index 39722f5c169..d703edd5512 100644 --- a/mcs/class/System.Json/System.Json/JsonValue.cs +++ b/mcs/class/System.Json/System.Json/JsonValue.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.Serialization.Json; @@ -328,70 +329,70 @@ namespace System.Json { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToBoolean (((JsonPrimitive) value).Value); + return Convert.ToBoolean (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator byte (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToByte (((JsonPrimitive) value).Value); + return Convert.ToByte (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator char (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToChar (((JsonPrimitive) value).Value); + return Convert.ToChar (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator decimal (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToDecimal (((JsonPrimitive) value).Value); + return Convert.ToDecimal (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator double (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToDouble (((JsonPrimitive) value).Value); + return Convert.ToDouble (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator float (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToSingle (((JsonPrimitive) value).Value); + return Convert.ToSingle (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator int (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToInt32 (((JsonPrimitive) value).Value); + return Convert.ToInt32 (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator long (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToInt64 (((JsonPrimitive) value).Value); + return Convert.ToInt64 (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator sbyte (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToSByte (((JsonPrimitive) value).Value); + return Convert.ToSByte (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator short (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToInt16 (((JsonPrimitive) value).Value); + return Convert.ToInt16 (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator string (JsonValue value) @@ -405,21 +406,21 @@ namespace System.Json { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToUInt16 (((JsonPrimitive) value).Value); + return Convert.ToUInt16 (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator ulong (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToUInt64(((JsonPrimitive) value).Value); + return Convert.ToUInt64(((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator ushort (JsonValue value) { if (value == null) throw new ArgumentNullException ("value"); - return Convert.ToUInt16 (((JsonPrimitive) value).Value); + return Convert.ToUInt16 (((JsonPrimitive) value).Value, NumberFormatInfo.InvariantInfo); } public static implicit operator DateTime (JsonValue value) diff --git a/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs b/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs index c3347c689d9..02dd106dfd8 100644 --- a/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs +++ b/mcs/class/System.Json/Test/System.Json/JsonValueTest.cs @@ -11,6 +11,8 @@ using System; using System.IO; using System.Text; using System.Json; +using System.Globalization; +using System.Threading; namespace MonoTests.System { @@ -24,6 +26,14 @@ namespace MonoTests.System Assert.AreEqual (1, j.Count, "itemcount"); Assert.AreEqual (JsonType.String, j ["a"].JsonType, "type"); Assert.AreEqual ("b", (string) j ["a"], "value"); + + JsonValue.Parse ("[{ \"a\": \"b\",}]"); + } + + [Test] + public void LoadWithTrailingComma2 () + { + JsonValue.Parse ("[{ \"a\": \"b\",}]"); } // Test that we correctly serialize JsonArray with null elements. @@ -41,5 +51,135 @@ namespace MonoTests.System { Assert.AreEqual ((new JsonPrimitive ("\"\"")).ToString (), "\"\\\"\\\"\""); } + + void ExpectError (string s) + { + try { + JsonValue.Parse (s); + Assert.Fail ("Expected ArgumentException for `" + s + "'"); + } catch (ArgumentException) { + } + } + + // Test whether an exception is thrown for invalid JSON + [Test] + public void CheckErrors () + { + ExpectError (@"-"); + ExpectError (@"- "); + ExpectError (@"1."); + ExpectError (@"1. "); + ExpectError (@"1e+"); + ExpectError (@"1 2"); + ExpectError (@"077"); + + ExpectError (@"[1,]"); + + //ExpectError (@"{""a"":1,}"); // Not valid JSON, allowed anyway + } + + // Parse a json string and compare to the expected value + void CheckDouble (double expected, string json) + { + double jvalue = (double) JsonValue.Parse (json); + Assert.AreEqual (expected, jvalue); + } + + // Convert a number to json and parse the string, then compare the result to the original value + void CheckDouble (double number) + { + double jvalue = (double) JsonValue.Parse (new JsonPrimitive (number).ToString ()); + Assert.AreEqual (number, jvalue); // should be exactly the same + } + + [Test] + public void CheckNumbers () + { + CheckDouble (0, "0"); + CheckDouble (0, "-0"); + CheckDouble (0, "0.00"); + CheckDouble (0, "-0.00"); + CheckDouble (1, "1"); + CheckDouble (1.1, "1.1"); + CheckDouble (-1, "-1"); + CheckDouble (-1.1, "-1.1"); + CheckDouble (1e-10, "1e-10"); + CheckDouble (1e+10, "1e+10"); + CheckDouble (1e-30, "1e-30"); + CheckDouble (1e+30, "1e+30"); + + CheckDouble (1, "\"1\""); + CheckDouble (1.1, "\"1.1\""); + CheckDouble (-1, "\"-1\""); + CheckDouble (-1.1, "\"-1.1\""); + + CheckDouble (double.NaN, "\"NaN\""); + CheckDouble (double.PositiveInfinity, "\"Infinity\""); + CheckDouble (double.NegativeInfinity, "\"-Infinity\""); + + ExpectError ("NaN"); + ExpectError ("Infinity"); + ExpectError ("-Infinity"); + + Assert.AreEqual ("1.1", new JsonPrimitive (1.1).ToString ()); + Assert.AreEqual ("-1.1", new JsonPrimitive (-1.1).ToString ()); + Assert.AreEqual ("1E-20", new JsonPrimitive (1e-20).ToString ()); + Assert.AreEqual ("1E+20", new JsonPrimitive (1e+20).ToString ()); + Assert.AreEqual ("1E-30", new JsonPrimitive (1e-30).ToString ()); + Assert.AreEqual ("1E+30", new JsonPrimitive (1e+30).ToString ()); + Assert.AreEqual ("\"NaN\"", new JsonPrimitive (double.NaN).ToString ()); + Assert.AreEqual ("\"Infinity\"", new JsonPrimitive (double.PositiveInfinity).ToString ()); + Assert.AreEqual ("\"-Infinity\"", new JsonPrimitive (double.NegativeInfinity).ToString ()); + + Assert.AreEqual ("1E-30", JsonValue.Parse ("1e-30").ToString ()); + Assert.AreEqual ("1E+30", JsonValue.Parse ("1e+30").ToString ()); + + CheckDouble (1); + CheckDouble (1.1); + CheckDouble (1.25); + CheckDouble (-1); + CheckDouble (-1.1); + CheckDouble (-1.25); + CheckDouble (1e-20); + CheckDouble (1e+20); + CheckDouble (1e-30); + CheckDouble (1e+30); + CheckDouble (3.1415926535897932384626433); + CheckDouble (3.1415926535897932384626433e-20); + CheckDouble (3.1415926535897932384626433e+20); + CheckDouble (double.NaN); + CheckDouble (double.PositiveInfinity); + CheckDouble (double.NegativeInfinity); + CheckDouble (double.MinValue); + CheckDouble (double.MaxValue); + + // A number which needs 17 digits (see http://stackoverflow.com/questions/6118231/why-do-i-need-17-significant-digits-and-not-16-to-represent-a-double) + CheckDouble (18014398509481982.0); + + // Values around the smallest positive decimal value + CheckDouble (1.123456789e-29); + CheckDouble (1.123456789e-28); + + CheckDouble (1.1E-29, "0.000000000000000000000000000011"); + // This is being parsed as a decimal and rounded to 1e-28, even though it can be more accurately be represented by a double + //CheckDouble (1.1E-28, "0.00000000000000000000000000011"); + } + + // Retry the test with different locales + [Test] + public void CheckNumbersCulture () + { + CultureInfo old = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo ("en"); + CheckNumbers (); + Thread.CurrentThread.CurrentCulture = new CultureInfo ("fr"); + CheckNumbers (); + Thread.CurrentThread.CurrentCulture = new CultureInfo ("de"); + CheckNumbers (); + } finally { + Thread.CurrentThread.CurrentCulture = old; + } + } } } diff --git a/mcs/class/System.ServiceModel.Web/System.Runtime.Serialization.Json/JavaScriptReader.cs b/mcs/class/System.ServiceModel.Web/System.Runtime.Serialization.Json/JavaScriptReader.cs index 891be5db3ee..2d11b1727ae 100644 --- a/mcs/class/System.ServiceModel.Web/System.Runtime.Serialization.Json/JavaScriptReader.cs +++ b/mcs/class/System.ServiceModel.Web/System.Runtime.Serialization.Json/JavaScriptReader.cs @@ -26,7 +26,7 @@ namespace System.Runtime.Serialization.Json { object v = ReadCore (); SkipSpaces (); - if (r.Read () >= 0) + if (ReadChar () >= 0) throw JsonError (String.Format ("extra characters in JSON input")); return v; } @@ -68,8 +68,10 @@ namespace System.Runtime.Serialization.Json } while (true) { SkipSpaces (); - if (PeekChar () == '}') + if (PeekChar () == '}') { + ReadChar (); break; + } string name = ReadStringLiteral (); SkipSpaces (); Expect (':'); @@ -160,98 +162,89 @@ namespace System.Runtime.Serialization.Json // It could return either int, long or decimal, depending on the parsed value. object ReadNumericLiteral () { + var sb = new StringBuilder (); + bool negative = false; if (PeekChar () == '-') { negative = true; - ReadChar (); - if (PeekChar () < 0) - throw JsonError ("Invalid JSON numeric literal; extra negation"); + sb.Append ((char) ReadChar ()); } int c; - decimal val = 0; int x = 0; bool zeroStart = PeekChar () == '0'; for (; ; x++) { c = PeekChar (); if (c < '0' || '9' < c) break; - val = val * 10 + (c - '0'); - ReadChar (); - if (zeroStart && x == 1 && c == '0') - throw JsonError ("leading multiple zeros are not allowed"); + sb.Append ((char) ReadChar ()); + if (zeroStart && x == 1) + throw JsonError ("leading zeros are not allowed"); } + if (x == 0) // Reached e.g. for "- " + throw JsonError ("Invalid JSON numeric literal; no digit found"); // fraction - bool hasFrac = false; - decimal frac = 0; int fdigits = 0; if (PeekChar () == '.') { hasFrac = true; - ReadChar (); + sb.Append ((char) ReadChar ()); if (PeekChar () < 0) throw JsonError ("Invalid JSON numeric literal; extra dot"); - decimal d = 10; while (true) { c = PeekChar (); if (c < '0' || '9' < c) break; - ReadChar (); - frac += (c - '0') / d; - d *= 10; + sb.Append ((char) ReadChar ()); fdigits++; } if (fdigits == 0) throw JsonError ("Invalid JSON numeric literal; extra dot"); } - frac = Decimal.Round (frac, fdigits); c = PeekChar (); if (c != 'e' && c != 'E') { if (!hasFrac) { - if (negative && int.MinValue <= -val || - !negative && val <= int.MaxValue) - return (int) (negative ? -val : val); - if (negative && long.MinValue <= -val || - !negative && val <= long.MaxValue) - return (long) (negative ? -val : val); + int valueInt; + if (int.TryParse (sb.ToString (), NumberStyles.Float, CultureInfo.InvariantCulture, out valueInt)) + return valueInt; + + long valueLong; + if (long.TryParse (sb.ToString (), NumberStyles.Float, CultureInfo.InvariantCulture, out valueLong)) + return valueLong; + + ulong valueUlong; + if (ulong.TryParse (sb.ToString (), NumberStyles.Float, CultureInfo.InvariantCulture, out valueUlong)) + return valueUlong; } - var v = val + frac; - return negative ? -v : v; - } - - // exponent - - ReadChar (); - - int exp = 0; - if (PeekChar () < 0) - throw new ArgumentException ("Invalid JSON numeric literal; incomplete exponent"); + decimal valueDecimal; + if (decimal.TryParse (sb.ToString (), NumberStyles.Float, CultureInfo.InvariantCulture, out valueDecimal) && valueDecimal != 0) + return valueDecimal; + } else { + // exponent + sb.Append ((char) ReadChar ()); + if (PeekChar () < 0) + throw new ArgumentException ("Invalid JSON numeric literal; incomplete exponent"); - bool negexp = false; - c = PeekChar (); - if (c == '-') { - ReadChar (); - negexp = true; - } - else if (c == '+') - ReadChar (); - - if (PeekChar () < 0) - throw JsonError ("Invalid JSON numeric literal; incomplete exponent"); - while (true) { c = PeekChar (); - if (c < '0' || '9' < c) - break; - exp = exp * 10 + (c - '0'); - ReadChar (); + if (c == '-') { + sb.Append ((char) ReadChar ()); + } + else if (c == '+') + sb.Append ((char) ReadChar ()); + + if (PeekChar () < 0) + throw JsonError ("Invalid JSON numeric literal; incomplete exponent"); + while (true) { + c = PeekChar (); + if (c < '0' || '9' < c) + break; + sb.Append ((char) ReadChar ()); + } } - // it is messy to handle exponent, so I just use Decimal.Parse() with assured JSON format. - if (negexp) - return new Decimal ((double) (val + frac) / Math.Pow (10, exp)); - int [] bits = Decimal.GetBits (val + frac); - return new Decimal (bits [0], bits [1], bits [2], negative, (byte) exp); + + return double.Parse (sb.ToString (), NumberStyles.Float, CultureInfo.InvariantCulture); } StringBuilder vb = new StringBuilder (); |