// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // Disable background compilation so that the Issue 24908 can be reproduced. // VMOptions=--no-background-compilation library json_test; import "package:expect/expect.dart"; import "dart:convert"; void testJson(jsonText, expected) { compare(expected, actual, path) { if (expected is List) { Expect.isTrue(actual is List); Expect.equals(expected.length, actual.length, "$path: List length"); for (int i = 0; i < expected.length; i++) { compare(expected[i], actual[i], "$path[$i] in $jsonText"); } } else if (expected is Map) { Expect.isTrue(actual is Map); Expect.equals(expected.length, actual.length, "$path: Map size in $jsonText"); expected.forEach((key, value) { Expect.isTrue(actual.containsKey(key)); compare(value, actual[key], "$path[$key] in $jsonText"); }); } else if (expected is num) { Expect.equals(expected is int, actual is int, "$path: not same number type in $jsonText"); Expect.isTrue(expected.compareTo(actual) == 0, "$path: Expected: $expected, was: $actual in $jsonText"); } else { // String, bool, null. Expect.equals(expected, actual, "$path in $jsonText"); } } for (var reviver in [null, (k, v) => v]) { for (var split in [0, 1, 2, 3, 4, 5]) { var name = (reviver == null) ? "" : "reviver:"; var sink = new ChunkedConversionSink.withCallback((values) { var value = values[0]; compare(expected, value, "$name$value"); }); var decoderSink = json.decoder.startChunkedConversion(sink); switch (split) { case 0: // Split after first char. decoderSink.add(jsonText.substring(0, 1)); decoderSink.add(jsonText.substring(1)); decoderSink.close(); break; case 1: // Split before last char. int length = jsonText.length; decoderSink.add(jsonText.substring(0, length - 1)); decoderSink.add(jsonText.substring(length - 1)); decoderSink.close(); break; case 2: // Split in middle. int half = jsonText.length ~/ 2; decoderSink.add(jsonText.substring(0, half)); decoderSink.add(jsonText.substring(half)); decoderSink.close(); break; case 3: // Split in three chunks. int length = jsonText.length; int third = length ~/ 3; decoderSink.add(jsonText.substring(0, third)); decoderSink.add(jsonText.substring(third, 2 * third)); decoderSink.add(jsonText.substring(2 * third)); decoderSink.close(); break; case 4: // Use .decode sink.add([json.decode(jsonText)]); break; case 5: // Use jsonDecode sink.add([jsonDecode(jsonText)]); break; } } } } String escape(String s) { var sb = new StringBuffer(); for (int i = 0; i < s.length; i++) { int code = s.codeUnitAt(i); if (code == '\\'.codeUnitAt(0)) sb.write(r'\\'); else if (code == '\"'.codeUnitAt(0)) sb.write(r'\"'); else if (code >= 32 && code < 127) sb.writeCharCode(code); else { String hex = '000${code.toRadixString(16)}'; sb.write(r'\u' '${hex.substring(hex.length - 4)}'); } } return '$sb'; } void testThrows(jsonText) { var message = "json = '${escape(jsonText)}'"; Expect.throwsFormatException(() => json.decode(jsonText), "json.decode, $message"); Expect.throwsFormatException(() => jsonDecode(jsonText), "jsonDecode, $message"); Expect.throwsFormatException(() => json.decoder.convert(jsonText), "json.decoder.convert, $message"); Expect.throwsFormatException(() => utf8.decoder.fuse(json.decoder).convert(utf8.encode(jsonText)), "utf8.decoder.fuse(json.decoder) o utf.encode, $message"); } testNumbers() { // Positive tests for number formats. var integerList = ["0", "9", "9999"]; var signList = ["", "-"]; var fractionList = ["", ".0", ".1", ".99999"]; var exponentList = [""]; for (var exphead in ["e", "E", "e-", "E-", "e+", "E+"]) { for (var expval in ["0", "1", "200"]) { exponentList.add("$exphead$expval"); } } for (var integer in integerList) { for (var sign in signList) { for (var fraction in fractionList) { for (var exp in exponentList) { for (var ws in ["", " ", "\t"]) { var literal = "$ws$sign$integer$fraction$exp$ws"; var expectedValue = num.parse(literal); testJson(literal, expectedValue); } } } } } // Regression test. // Detect and handle overflow on integer literals by making them doubles // (work like `num.parse`). testJson("9223372036854774784", 9223372036854774784); testJson("-9223372036854775808", -9223372036854775808); testJson("9223372036854775808", 9223372036854775808.0); testJson("-9223372036854775809", -9223372036854775809.0); testJson("9223372036854775808.0", 9223372036854775808.0); testJson("9223372036854775810", 9223372036854775810.0); testJson("18446744073709551616.0", 18446744073709551616.0); testJson("1e309", double.infinity); testJson("-1e309", double.negativeInfinity); testJson("1e-325", 0.0); testJson("-1e-325", -0.0); // No overflow on exponent. testJson("1e18446744073709551616", double.infinity); testJson("-1e18446744073709551616", double.negativeInfinity); testJson("1e-18446744073709551616", 0.0); testJson("-1e-18446744073709551616", -0.0); // (Wrapping numbers in list because the chunked parsing handles top-level // numbers by buffering and then parsing using platform parser). testJson("[9223372036854774784]", [9223372036854774784]); testJson("[-9223372036854775808]", [-9223372036854775808]); testJson("[9223372036854775808]", [9223372036854775808.0]); testJson("[-9223372036854775809]", [-9223372036854775809.0]); testJson("[9223372036854775808.0]", [9223372036854775808.0]); testJson("[9223372036854775810]", [9223372036854775810.0]); testJson("[18446744073709551616.0]", [18446744073709551616.0]); testJson("[1e309]", [double.infinity]); testJson("[-1e309]", [double.negativeInfinity]); testJson("[1e-325]", [0.0]); testJson("[-1e-325]", [-0.0]); // No overflow on exponent. testJson("[1e18446744073709551616]", [double.infinity]); testJson("[-1e18446744073709551616]", [double.negativeInfinity]); testJson("[1e-18446744073709551616]", [0.0]); testJson("[-1e-18446744073709551616]", [-0.0]); // Negative tests (syntax error). // testError thoroughly tests the given parts with a lot of valid // values for the other parts. testError({signs, integers, fractions, exponents}) { def(value, defaultValue) { if (value == null) return defaultValue; if (value is List) return value; return [value]; } signs = def(signs, signList); integers = def(integers, integerList); fractions = def(fractions, fractionList); exponents = def(exponents, exponentList); for (var integer in integers) { for (var sign in signs) { for (var fraction in fractions) { for (var exponent in exponents) { var literal = "$sign$integer$fraction$exponent"; testThrows(literal); } } } } } // Doubles overflow to Infinity. testJson("1e+400", double.infinity); // (Integers do not, but we don't have those on dart2js). // Integer part cannot be omitted: testError(integers: ""); // Test for "Initial zero only allowed for zero integer part" moved to // json_strict_test.dart because IE's jsonDecode accepts additional initial // zeros. // Only minus allowed as sign. testError(signs: "+"); // Requires digits after decimal point. testError(fractions: "."); // Requires exponent digts, and only digits. testError(exponents: ["e", "e+", "e-", "e.0"]); // No whitespace inside numbers. // Additional case "- 2.2e+2" in json_strict_test.dart. testThrows("-2 .2e+2"); testThrows("-2. 2e+2"); testThrows("-2.2 e+2"); testThrows("-2.2e +2"); testThrows("-2.2e+ 2"); testThrows("01"); testThrows("0."); testThrows(".0"); testThrows("0.e1"); testThrows("[2.,2]"); testThrows("{2.:2}"); testThrows("NaN"); testThrows("Infinity"); testThrows("-Infinity"); Expect.throws(() => json.encode(double.nan)); Expect.throws(() => json.encode(double.infinity)); Expect.throws(() => json.encode(double.negativeInfinity)); Expect.throws(() => jsonEncode(double.nan)); Expect.throws(() => jsonEncode(double.infinity)); Expect.throws(() => jsonEncode(double.negativeInfinity)); } testStrings() { // String parser accepts and understands escapes. var input = r'"\u0000\uffff\n\r\f\t\b\/\\\"' '\x20\ufffd\uffff"'; var expected = "\u0000\uffff\n\r\f\t\b\/\\\"\x20\ufffd\uffff"; testJson(input, expected); // Empty string. testJson(r'""', ""); // Escape first. var escapes = { "f": "\f", "b": "\b", "n": "\n", "r": "\r", "t": "\t", r"\": r"\", '"': '"', "/": "/", }; escapes.forEach((esc, lit) { testJson('"\\$esc........"', "$lit........"); // Escape last. testJson('"........\\$esc"', "........$lit"); // Escape middle. testJson('"....\\$esc...."', "....$lit...."); }); // Does not accept single quotes. testThrows(r"''"); // Throws on unterminated strings. testThrows(r'"......\"'); // Throws on unterminated escapes. testThrows(r'"\'); // ' is not escaped. testThrows(r'"\a"'); testThrows(r'"\u"'); testThrows(r'"\u1"'); testThrows(r'"\u12"'); testThrows(r'"\u123"'); testThrows(r'"\ux"'); testThrows(r'"\u1x"'); testThrows(r'"\u12x"'); testThrows(r'"\u123x"'); // Throws on bad escapes. testThrows(r'"\a"'); testThrows(r'"\x00"'); testThrows(r'"\c2"'); testThrows(r'"\000"'); testThrows(r'"\u{0}"'); testThrows(r'"\%"'); testThrows('"\\\x00"'); // Not raw string! // Throws on control characters. for (int i = 0; i < 32; i++) { var string = new String.fromCharCodes([0x22, i, 0x22]); // '"\x00"' etc. testThrows(string); } } testObjects() { testJson(r'{}', {}); testJson(r'{"x":42}', {"x": 42}); testJson(r'{"x":{"x":{"x":42}}}', { "x": { "x": {"x": 42} } }); testJson(r'{"x":10,"x":42}', {"x": 42}); testJson(r'{"":42}', {"": 42}); // Keys must be strings. testThrows(r'{x:10}'); testThrows(r'{true:10}'); testThrows(r'{false:10}'); testThrows(r'{null:10}'); testThrows(r'{42:10}'); testThrows(r'{42e1:10}'); testThrows(r'{-42:10}'); testThrows(r'{["text"]:10}'); testThrows(r'{:10}'); } testArrays() { testJson(r'[]', []); testJson(r'[1.1e1,"string",true,false,null,{}]', [1.1e1, "string", true, false, null, {}]); testJson(r'[[[[[[]]]],[[[]]],[[]]]]', [ [ [ [ [[]] ] ], [ [[]] ], [[]] ] ]); testJson(r'[{},[{}],{"x":[]}]', [ {}, [{}], {"x": []} ]); testThrows(r'[1,,2]'); testThrows(r'[1,2,]'); testThrows(r'[,2]'); } testWords() { testJson(r'true', true); testJson(r'false', false); testJson(r'null', null); testJson(r'[true]', [true]); testJson(r'{"true":true}', {"true": true}); testThrows(r'truefalse'); testThrows(r'trues'); testThrows(r'nulll'); testThrows(r'full'); testThrows(r'nul'); testThrows(r'tru'); testThrows(r'fals'); testThrows(r'\null'); testThrows(r't\rue'); testThrows(r't\rue'); } testWhitespace() { // Valid white-space characters. var v = '\t\r\n\ '; // Invalid white-space and non-recognized characters. var invalids = ['\x00', '\f', '\x08', '\\', '\xa0', '\u2028', '\u2029']; // Valid whitespace accepted "everywhere". testJson('$v[${v}-2.2e2$v,$v{$v"key"$v:${v}true$v}$v,$v"ab"$v]$v', [ -2.2e2, {"key": true}, "ab" ]); // IE9 accepts invalid characters at the end, so some of these tests have been // moved to json_strict_test.dart. for (var i in invalids) { testThrows('${i}"s"'); testThrows('42${i}'); testThrows('$i[]'); testThrows('[$i]'); testThrows('[$i"s"]'); testThrows('["s"$i]'); testThrows('$i{"k":"v"}'); testThrows('{$i"k":"v"}'); testThrows('{"k"$i:"v"}'); testThrows('{"k":$i"v"}'); testThrows('{"k":"v"$i}'); } } main() { testNumbers(); testStrings(); testWords(); testObjects(); testArrays(); testWhitespace(); }