Fix several issues in System.Json
authorSteffen Kieß <Steffen.Kiess@ipvs.uni-stuttgart.de>
Mon, 7 Jul 2014 17:22:29 +0000 (19:22 +0200)
committerSteffen Kieß <Steffen.Kiess@ipvs.uni-stuttgart.de>
Mon, 7 Jul 2014 17:22:29 +0000 (19:22 +0200)
- 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

mcs/class/System.Json/System.Json/JsonPrimitive.cs
mcs/class/System.Json/System.Json/JsonValue.cs
mcs/class/System.Json/Test/System.Json/JsonValueTest.cs
mcs/class/System.ServiceModel.Web/System.Runtime.Serialization.Json/JavaScriptReader.cs

index 64518785b87351fa6797c4bdfd8feb170df85a57..5d47eb4a4e6b6875a8cc103d914b56ee10cac2f4 100644 (file)
@@ -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 ();
                        }
index 39722f5c16937b059caa4d3f998af28a4633ee61..d703edd5512c5c6427f43f78a2975f8600f5edd5 100644 (file)
@@ -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)
index c3347c689d9999f8bba2514beb75f6c2fedb7098..02dd106dfd8b82a5b3f3f9bf2ea71663d7aead57 100644 (file)
@@ -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;
+                       }
+               }
        }
 }
index 891be5db3ee2b0c44fa287bbad83c4e0e9b6e818..2d11b1727aec81a2f5f1b1296e1b4336c19296f4 100644 (file)
@@ -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 ();