phobos/std/json.d
Tyler Knott 20353df3d9 Fix issue 16639 - Review std.json wrt this article on JSON edge cases and ambiguities
The test corpus provided at https://github.com/nst/JSONTestSuite/ revealed some issues with the std.json.parseJSON function. Since addressing some of the issues required parseJSON to reject input it previously accepted, I have added a new JSONOptions.strictParsing flag so callers can opt-in to the stricter parsing.

The issues, and how I've addressed them, are listed below (approximately from most severe to least):

Silently dropping ASCII NUL characters from strings:
n_string_unescaped_crtl_char.json

This is the most serious problem I found while fixing the test cases. The current implementation of parseJSON() uses a helper function called peekChar() which can store the next character to handle in a variable of type Char (an alias of the character type). Unfortunately it was using 0 to indicate it has not read a character yet so if an ASCII NUL (which will have the value 0) is present in the text and someone reads it with peekChar() then it will effectively be skipped over, which was happening in string and whitespace parsing.

I changed peekChar() to use a Nullable!Char as the temporary storage for the next character to disambiguate the case where there is no pending unconsumed character from the case where there is a pending unconsumed ASCII NUL. In strict mode JSON with unescaped ASCII NULs in strings will throw an exception while in non-strict mode the JSON will be accepted with the NUL included in the string value.

Failure to accept ASCII DEL (0x7f) unescaped in strings:
y_string_unescaped_char_delete.json
y_string_with_del_character.json

These were the only test cases that std.json rejected that it should have accepted. This issue was addressed by changing the string parsing logic to explicitly check for character values < 0x20 instead of using std.ascii.isControl (which also returned true for 0x7f), with a special exception for ASCII NULs in non-strict mode as mentioned above.

Parsing "true", "false", and "null" tokens case-insensitively:
n_structure_capitalized_True.json

In strict mode those tokens are now parsed case-sensitively.

Accepting control characters other than ' ', '\t', '\r', and '\n' as whitespace:
n_structure_null-byte-outside-string.json
n_structure_whitespace_formfeed.json

In strict mode only the listed characters are accepted as whitespace, while non-strict mode continues to use std.ascii.isWhite with an additional exception for ASCII NUL for a similar reason as the n_string_unescaped_ctrl_char.json case (the skipWhitespace() function used peekChar() so it didn't handle ASCII NULs consistently; non-strict mode after my changes is actually more permissive than the previous behavior but it is at least consistently permissive).

Silently accepting empty data:
n_structure_no_data.json

In strict mode an exception is now thrown instead of returning an empty value.

Failure to enforce that numbers beginning with 0 cannot have any additional digits in the non-fractional part:
n_number_-01.json
n_number_neg_int_starting_with_zero.json
n_number_with_leading_zero.json

An additional check is now performed in strict mode when the whole part of a number begins with zero to ensure trailing digits are not present.

Failure to check for trailing characters after parsing:
n_array_comma_after_close.json
n_array_extra_close.json
n_multidigit_number_then_00.json
n_object_trailing_comment.json
n_object_trailing_comment_open.json
n_object_trailing_comment_slash_open_incomplete.json
n_object_trailing_comment_slash_open.json
n_object_with_trailing_garbage.json
n_string_with_trailing_garbage.json
n_structure_array_trailing_garbage.json
n_structure_array_with_extra_array_close.json
n_structure_close_unopened_array.json
n_structure_double_array.json
n_structure_number_with_trailing_garbage.json
n_structure_object_followed_by_closing_object.json
n_structure_object_with_trailing_garbage.json
n_structure_trailing_#.json

An additional check is now performed in strict mode to ensure any trailing characters after the initial JSON value are only whitespace.

In addition to the above issues, parseJSON() will throw ConvException for numbers out of the range of double/long/ulong which was not previously documented. I have updated the ddoc comment to reference that exception.
2018-06-27 21:29:00 -05:00

1985 lines
58 KiB
D

// Written in the D programming language.
/**
JavaScript Object Notation
Copyright: Copyright Jeremie Pelletier 2008 - 2009.
License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
Authors: Jeremie Pelletier, David Herberth
References: $(LINK http://json.org/), $(LINK http://seriot.ch/parsing_json.html)
Source: $(PHOBOSSRC std/json.d)
*/
/*
Copyright Jeremie Pelletier 2008 - 2009.
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)
*/
module std.json;
import std.array;
import std.conv;
import std.range.primitives;
import std.traits;
///
@system unittest
{
import std.conv : to;
// parse a file or string of json into a usable structure
string s = `{ "language": "D", "rating": 3.5, "code": "42" }`;
JSONValue j = parseJSON(s);
// j and j["language"] return JSONValue,
// j["language"].str returns a string
assert(j["language"].str == "D");
assert(j["rating"].floating == 3.5);
// check a type
long x;
if (const(JSONValue)* code = "code" in j)
{
if (code.type() == JSON_TYPE.INTEGER)
x = code.integer;
else
x = to!int(code.str);
}
// create a json struct
JSONValue jj = [ "language": "D" ];
// rating doesnt exist yet, so use .object to assign
jj.object["rating"] = JSONValue(3.5);
// create an array to assign to list
jj.object["list"] = JSONValue( ["a", "b", "c"] );
// list already exists, so .object optional
jj["list"].array ~= JSONValue("D");
string jjStr = `{"language":"D","list":["a","b","c","D"],"rating":3.5}`;
assert(jj.toString == jjStr);
}
/**
String literals used to represent special float values within JSON strings.
*/
enum JSONFloatLiteral : string
{
nan = "NaN", /// string representation of floating-point NaN
inf = "Infinite", /// string representation of floating-point Infinity
negativeInf = "-Infinite", /// string representation of floating-point negative Infinity
}
/**
Flags that control how json is encoded and parsed.
*/
enum JSONOptions
{
none, /// standard parsing
specialFloatLiterals = 0x1, /// encode NaN and Inf float values as strings
escapeNonAsciiChars = 0x2, /// encode non ascii characters with an unicode escape sequence
doNotEscapeSlashes = 0x4, /// do not escape slashes ('/')
strictParsing = 0x8, /// Strictly follow RFC-8259 grammar when parsing
}
/**
JSON type enumeration
*/
enum JSON_TYPE : byte
{
/// Indicates the type of a `JSONValue`.
NULL,
STRING, /// ditto
INTEGER, /// ditto
UINTEGER,/// ditto
FLOAT, /// ditto
OBJECT, /// ditto
ARRAY, /// ditto
TRUE, /// ditto
FALSE /// ditto
}
/**
JSON value node
*/
struct JSONValue
{
import std.exception : enforce;
union Store
{
string str;
long integer;
ulong uinteger;
double floating;
JSONValue[string] object;
JSONValue[] array;
}
private Store store;
private JSON_TYPE type_tag;
/**
Returns the JSON_TYPE of the value stored in this structure.
*/
@property JSON_TYPE type() const pure nothrow @safe @nogc
{
return type_tag;
}
///
@safe unittest
{
string s = "{ \"language\": \"D\" }";
JSONValue j = parseJSON(s);
assert(j.type == JSON_TYPE.OBJECT);
assert(j["language"].type == JSON_TYPE.STRING);
}
/***
* Value getter/setter for `JSON_TYPE.STRING`.
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.STRING`.
*/
@property string str() const pure @trusted
{
enforce!JSONException(type == JSON_TYPE.STRING,
"JSONValue is not a string");
return store.str;
}
/// ditto
@property string str(string v) pure nothrow @nogc @safe
{
assign(v);
return v;
}
///
@safe unittest
{
JSONValue j = [ "language": "D" ];
// get value
assert(j["language"].str == "D");
// change existing key to new string
j["language"].str = "Perl";
assert(j["language"].str == "Perl");
}
/***
* Value getter/setter for `JSON_TYPE.INTEGER`.
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.INTEGER`.
*/
@property inout(long) integer() inout pure @safe
{
enforce!JSONException(type == JSON_TYPE.INTEGER,
"JSONValue is not an integer");
return store.integer;
}
/// ditto
@property long integer(long v) pure nothrow @safe @nogc
{
assign(v);
return store.integer;
}
/***
* Value getter/setter for `JSON_TYPE.UINTEGER`.
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.UINTEGER`.
*/
@property inout(ulong) uinteger() inout pure @safe
{
enforce!JSONException(type == JSON_TYPE.UINTEGER,
"JSONValue is not an unsigned integer");
return store.uinteger;
}
/// ditto
@property ulong uinteger(ulong v) pure nothrow @safe @nogc
{
assign(v);
return store.uinteger;
}
/***
* Value getter/setter for `JSON_TYPE.FLOAT`. Note that despite
* the name, this is a $(B 64)-bit `double`, not a 32-bit `float`.
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.FLOAT`.
*/
@property inout(double) floating() inout pure @safe
{
enforce!JSONException(type == JSON_TYPE.FLOAT,
"JSONValue is not a floating type");
return store.floating;
}
/// ditto
@property double floating(double v) pure nothrow @safe @nogc
{
assign(v);
return store.floating;
}
/***
* Value getter/setter for `JSON_TYPE.OBJECT`.
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.OBJECT`.
* Note: this is @system because of the following pattern:
---
auto a = &(json.object());
json.uinteger = 0; // overwrite AA pointer
(*a)["hello"] = "world"; // segmentation fault
---
*/
@property ref inout(JSONValue[string]) object() inout pure @system
{
enforce!JSONException(type == JSON_TYPE.OBJECT,
"JSONValue is not an object");
return store.object;
}
/// ditto
@property JSONValue[string] object(JSONValue[string] v) pure nothrow @nogc @safe
{
assign(v);
return v;
}
/***
* Value getter for `JSON_TYPE.OBJECT`.
* Unlike `object`, this retrieves the object by value and can be used in @safe code.
*
* A caveat is that, if the returned value is null, modifications will not be visible:
* ---
* JSONValue json;
* json.object = null;
* json.objectNoRef["hello"] = JSONValue("world");
* assert("hello" !in json.object);
* ---
*
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.OBJECT`.
*/
@property inout(JSONValue[string]) objectNoRef() inout pure @trusted
{
enforce!JSONException(type == JSON_TYPE.OBJECT,
"JSONValue is not an object");
return store.object;
}
/***
* Value getter/setter for `JSON_TYPE.ARRAY`.
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.ARRAY`.
* Note: this is @system because of the following pattern:
---
auto a = &(json.array());
json.uinteger = 0; // overwrite array pointer
(*a)[0] = "world"; // segmentation fault
---
*/
@property ref inout(JSONValue[]) array() inout pure @system
{
enforce!JSONException(type == JSON_TYPE.ARRAY,
"JSONValue is not an array");
return store.array;
}
/// ditto
@property JSONValue[] array(JSONValue[] v) pure nothrow @nogc @safe
{
assign(v);
return v;
}
/***
* Value getter for `JSON_TYPE.ARRAY`.
* Unlike `array`, this retrieves the array by value and can be used in @safe code.
*
* A caveat is that, if you append to the returned array, the new values aren't visible in the
* JSONValue:
* ---
* JSONValue json;
* json.array = [JSONValue("hello")];
* json.arrayNoRef ~= JSONValue("world");
* assert(json.array.length == 1);
* ---
*
* Throws: `JSONException` for read access if `type` is not
* `JSON_TYPE.ARRAY`.
*/
@property inout(JSONValue[]) arrayNoRef() inout pure @trusted
{
enforce!JSONException(type == JSON_TYPE.ARRAY,
"JSONValue is not an array");
return store.array;
}
/// Test whether the type is `JSON_TYPE.NULL`
@property bool isNull() const pure nothrow @safe @nogc
{
return type == JSON_TYPE.NULL;
}
private void assign(T)(T arg) @safe
{
static if (is(T : typeof(null)))
{
type_tag = JSON_TYPE.NULL;
}
else static if (is(T : string))
{
type_tag = JSON_TYPE.STRING;
string t = arg;
() @trusted { store.str = t; }();
}
else static if (isSomeString!T) // issue 15884
{
type_tag = JSON_TYPE.STRING;
// FIXME: std.array.array(Range) is not deduced as 'pure'
() @trusted {
import std.utf : byUTF;
store.str = cast(immutable)(arg.byUTF!char.array);
}();
}
else static if (is(T : bool))
{
type_tag = arg ? JSON_TYPE.TRUE : JSON_TYPE.FALSE;
}
else static if (is(T : ulong) && isUnsigned!T)
{
type_tag = JSON_TYPE.UINTEGER;
store.uinteger = arg;
}
else static if (is(T : long))
{
type_tag = JSON_TYPE.INTEGER;
store.integer = arg;
}
else static if (isFloatingPoint!T)
{
type_tag = JSON_TYPE.FLOAT;
store.floating = arg;
}
else static if (is(T : Value[Key], Key, Value))
{
static assert(is(Key : string), "AA key must be string");
type_tag = JSON_TYPE.OBJECT;
static if (is(Value : JSONValue))
{
JSONValue[string] t = arg;
() @trusted { store.object = t; }();
}
else
{
JSONValue[string] aa;
foreach (key, value; arg)
aa[key] = JSONValue(value);
() @trusted { store.object = aa; }();
}
}
else static if (isArray!T)
{
type_tag = JSON_TYPE.ARRAY;
static if (is(ElementEncodingType!T : JSONValue))
{
JSONValue[] t = arg;
() @trusted { store.array = t; }();
}
else
{
JSONValue[] new_arg = new JSONValue[arg.length];
foreach (i, e; arg)
new_arg[i] = JSONValue(e);
() @trusted { store.array = new_arg; }();
}
}
else static if (is(T : JSONValue))
{
type_tag = arg.type;
store = arg.store;
}
else
{
static assert(false, text(`unable to convert type "`, T.stringof, `" to json`));
}
}
private void assignRef(T)(ref T arg) if (isStaticArray!T)
{
type_tag = JSON_TYPE.ARRAY;
static if (is(ElementEncodingType!T : JSONValue))
{
store.array = arg;
}
else
{
JSONValue[] new_arg = new JSONValue[arg.length];
foreach (i, e; arg)
new_arg[i] = JSONValue(e);
store.array = new_arg;
}
}
/**
* Constructor for `JSONValue`. If `arg` is a `JSONValue`
* its value and type will be copied to the new `JSONValue`.
* Note that this is a shallow copy: if type is `JSON_TYPE.OBJECT`
* or `JSON_TYPE.ARRAY` then only the reference to the data will
* be copied.
* Otherwise, `arg` must be implicitly convertible to one of the
* following types: `typeof(null)`, `string`, `ulong`,
* `long`, `double`, an associative array `V[K]` for any `V`
* and `K` i.e. a JSON object, any array or `bool`. The type will
* be set accordingly.
*/
this(T)(T arg) if (!isStaticArray!T)
{
assign(arg);
}
/// Ditto
this(T)(ref T arg) if (isStaticArray!T)
{
assignRef(arg);
}
/// Ditto
this(T : JSONValue)(inout T arg) inout
{
store = arg.store;
type_tag = arg.type;
}
///
@safe unittest
{
JSONValue j = JSONValue( "a string" );
j = JSONValue(42);
j = JSONValue( [1, 2, 3] );
assert(j.type == JSON_TYPE.ARRAY);
j = JSONValue( ["language": "D"] );
assert(j.type == JSON_TYPE.OBJECT);
}
void opAssign(T)(T arg) if (!isStaticArray!T && !is(T : JSONValue))
{
assign(arg);
}
void opAssign(T)(ref T arg) if (isStaticArray!T)
{
assignRef(arg);
}
/***
* Array syntax for json arrays.
* Throws: `JSONException` if `type` is not `JSON_TYPE.ARRAY`.
*/
ref inout(JSONValue) opIndex(size_t i) inout pure @safe
{
auto a = this.arrayNoRef;
enforce!JSONException(i < a.length,
"JSONValue array index is out of range");
return a[i];
}
///
@safe unittest
{
JSONValue j = JSONValue( [42, 43, 44] );
assert( j[0].integer == 42 );
assert( j[1].integer == 43 );
}
/***
* Hash syntax for json objects.
* Throws: `JSONException` if `type` is not `JSON_TYPE.OBJECT`.
*/
ref inout(JSONValue) opIndex(string k) inout pure @safe
{
auto o = this.objectNoRef;
return *enforce!JSONException(k in o,
"Key not found: " ~ k);
}
///
@safe unittest
{
JSONValue j = JSONValue( ["language": "D"] );
assert( j["language"].str == "D" );
}
/***
* Operator sets `value` for element of JSON object by `key`.
*
* If JSON value is null, then operator initializes it with object and then
* sets `value` for it.
*
* Throws: `JSONException` if `type` is not `JSON_TYPE.OBJECT`
* or `JSON_TYPE.NULL`.
*/
void opIndexAssign(T)(auto ref T value, string key) pure
{
enforce!JSONException(type == JSON_TYPE.OBJECT || type == JSON_TYPE.NULL,
"JSONValue must be object or null");
JSONValue[string] aa = null;
if (type == JSON_TYPE.OBJECT)
{
aa = this.objectNoRef;
}
aa[key] = value;
this.object = aa;
}
///
@safe unittest
{
JSONValue j = JSONValue( ["language": "D"] );
j["language"].str = "Perl";
assert( j["language"].str == "Perl" );
}
void opIndexAssign(T)(T arg, size_t i) pure
{
auto a = this.arrayNoRef;
enforce!JSONException(i < a.length,
"JSONValue array index is out of range");
a[i] = arg;
this.array = a;
}
///
@safe unittest
{
JSONValue j = JSONValue( ["Perl", "C"] );
j[1].str = "D";
assert( j[1].str == "D" );
}
JSONValue opBinary(string op : "~", T)(T arg) @safe
{
auto a = this.arrayNoRef;
static if (isArray!T)
{
return JSONValue(a ~ JSONValue(arg).arrayNoRef);
}
else static if (is(T : JSONValue))
{
return JSONValue(a ~ arg.arrayNoRef);
}
else
{
static assert(false, "argument is not an array or a JSONValue array");
}
}
void opOpAssign(string op : "~", T)(T arg) @safe
{
auto a = this.arrayNoRef;
static if (isArray!T)
{
a ~= JSONValue(arg).arrayNoRef;
}
else static if (is(T : JSONValue))
{
a ~= arg.arrayNoRef;
}
else
{
static assert(false, "argument is not an array or a JSONValue array");
}
this.array = a;
}
/**
* Support for the `in` operator.
*
* Tests wether a key can be found in an object.
*
* Returns:
* when found, the `const(JSONValue)*` that matches to the key,
* otherwise `null`.
*
* Throws: `JSONException` if the right hand side argument `JSON_TYPE`
* is not `OBJECT`.
*/
auto opBinaryRight(string op : "in")(string k) const @safe
{
return k in this.objectNoRef;
}
///
@safe unittest
{
JSONValue j = [ "language": "D", "author": "walter" ];
string a = ("author" in j).str;
}
bool opEquals(const JSONValue rhs) const @nogc nothrow pure @safe
{
return opEquals(rhs);
}
bool opEquals(ref const JSONValue rhs) const @nogc nothrow pure @trusted
{
// Default doesn't work well since store is a union. Compare only
// what should be in store.
// This is @trusted to remain nogc, nothrow, fast, and usable from @safe code.
if (type_tag != rhs.type_tag) return false;
final switch (type_tag)
{
case JSON_TYPE.STRING:
return store.str == rhs.store.str;
case JSON_TYPE.INTEGER:
return store.integer == rhs.store.integer;
case JSON_TYPE.UINTEGER:
return store.uinteger == rhs.store.uinteger;
case JSON_TYPE.FLOAT:
return store.floating == rhs.store.floating;
case JSON_TYPE.OBJECT:
return store.object == rhs.store.object;
case JSON_TYPE.ARRAY:
return store.array == rhs.store.array;
case JSON_TYPE.TRUE:
case JSON_TYPE.FALSE:
case JSON_TYPE.NULL:
return true;
}
}
/// Implements the foreach `opApply` interface for json arrays.
int opApply(scope int delegate(size_t index, ref JSONValue) dg) @system
{
int result;
foreach (size_t index, ref value; array)
{
result = dg(index, value);
if (result)
break;
}
return result;
}
/// Implements the foreach `opApply` interface for json objects.
int opApply(scope int delegate(string key, ref JSONValue) dg) @system
{
enforce!JSONException(type == JSON_TYPE.OBJECT,
"JSONValue is not an object");
int result;
foreach (string key, ref value; object)
{
result = dg(key, value);
if (result)
break;
}
return result;
}
/***
* Implicitly calls `toJSON` on this JSONValue.
*
* $(I options) can be used to tweak the conversion behavior.
*/
string toString(in JSONOptions options = JSONOptions.none) const @safe
{
return toJSON(this, false, options);
}
/***
* Implicitly calls `toJSON` on this JSONValue, like `toString`, but
* also passes $(I true) as $(I pretty) argument.
*
* $(I options) can be used to tweak the conversion behavior
*/
string toPrettyString(in JSONOptions options = JSONOptions.none) const @safe
{
return toJSON(this, true, options);
}
}
/**
Parses a serialized string and returns a tree of JSON values.
Throws: $(LREF JSONException) if string does not follow the JSON grammar or the depth exceeds the max depth,
$(LREF ConvException) if a number in the input cannot be represented by a native D type.
Params:
json = json-formatted string to parse
maxDepth = maximum depth of nesting allowed, -1 disables depth checking
options = enable decoding string representations of NaN/Inf as float values
*/
JSONValue parseJSON(T)(T json, int maxDepth = -1, JSONOptions options = JSONOptions.none)
if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T))
{
import std.ascii : isDigit, isHexDigit, toUpper, toLower;
import std.typecons : Nullable, Yes;
JSONValue root;
root.type_tag = JSON_TYPE.NULL;
// Avoid UTF decoding when possible, as it is unnecessary when
// processing JSON.
static if (is(T : const(char)[]))
alias Char = char;
else
alias Char = Unqual!(ElementType!T);
int depth = -1;
Nullable!Char next;
int line = 1, pos = 0;
immutable bool strict = (options & JSONOptions.strictParsing) != 0;
void error(string msg)
{
throw new JSONException(msg, line, pos);
}
if (json.empty)
{
if (strict)
{
error("Empty JSON body");
}
return root;
}
bool isWhite(dchar c)
{
if (strict)
{
// RFC 7159 has a stricter definition of whitespace than general ASCII.
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
import std.ascii : isWhite;
// Accept ASCII NUL as whitespace in non-strict mode.
return c == 0 || isWhite(c);
}
Char popChar()
{
if (json.empty) error("Unexpected end of data.");
static if (is(T : const(char)[]))
{
Char c = json[0];
json = json[1..$];
}
else
{
Char c = json.front;
json.popFront();
}
if (c == '\n')
{
line++;
pos = 0;
}
else
{
pos++;
}
return c;
}
Char peekChar()
{
if (next.isNull)
{
if (json.empty) return '\0';
next = popChar();
}
return next.get;
}
Nullable!Char peekCharNullable()
{
if (next.isNull && !json.empty)
{
next = popChar();
}
return next;
}
void skipWhitespace()
{
while (true)
{
auto c = peekCharNullable();
if (c.isNull ||
!isWhite(c.get))
{
return;
}
next.nullify();
}
}
Char getChar(bool SkipWhitespace = false)()
{
static if (SkipWhitespace) skipWhitespace();
Char c;
if (!next.isNull)
{
c = next.get;
next.nullify();
}
else
c = popChar();
return c;
}
void checkChar(bool SkipWhitespace = true)(char c, bool caseSensitive = true)
{
static if (SkipWhitespace) skipWhitespace();
auto c2 = getChar();
if (!caseSensitive) c2 = toLower(c2);
if (c2 != c) error(text("Found '", c2, "' when expecting '", c, "'."));
}
bool testChar(bool SkipWhitespace = true, bool CaseSensitive = true)(char c)
{
static if (SkipWhitespace) skipWhitespace();
auto c2 = peekChar();
static if (!CaseSensitive) c2 = toLower(c2);
if (c2 != c) return false;
getChar();
return true;
}
wchar parseWChar()
{
wchar val = 0;
foreach_reverse (i; 0 .. 4)
{
auto hex = toUpper(getChar());
if (!isHexDigit(hex)) error("Expecting hex character");
val += (isDigit(hex) ? hex - '0' : hex - ('A' - 10)) << (4 * i);
}
return val;
}
string parseString()
{
import std.uni : isSurrogateHi, isSurrogateLo;
import std.utf : encode, decode;
auto str = appender!string();
Next:
switch (peekChar())
{
case '"':
getChar();
break;
case '\\':
getChar();
auto c = getChar();
switch (c)
{
case '"': str.put('"'); break;
case '\\': str.put('\\'); break;
case '/': str.put('/'); break;
case 'b': str.put('\b'); break;
case 'f': str.put('\f'); break;
case 'n': str.put('\n'); break;
case 'r': str.put('\r'); break;
case 't': str.put('\t'); break;
case 'u':
wchar wc = parseWChar();
dchar val;
// Non-BMP characters are escaped as a pair of
// UTF-16 surrogate characters (see RFC 4627).
if (isSurrogateHi(wc))
{
wchar[2] pair;
pair[0] = wc;
if (getChar() != '\\') error("Expected escaped low surrogate after escaped high surrogate");
if (getChar() != 'u') error("Expected escaped low surrogate after escaped high surrogate");
pair[1] = parseWChar();
size_t index = 0;
val = decode(pair[], index);
if (index != 2) error("Invalid escaped surrogate pair");
}
else
if (isSurrogateLo(wc))
error(text("Unexpected low surrogate"));
else
val = wc;
char[4] buf;
immutable len = encode!(Yes.useReplacementDchar)(buf, val);
str.put(buf[0 .. len]);
break;
default:
error(text("Invalid escape sequence '\\", c, "'."));
}
goto Next;
default:
// RFC 7159 states that control characters U+0000 through
// U+001F must not appear unescaped in a JSON string.
// Note: std.ascii.isControl can't be used for this test
// because it considers ASCII DEL (0x7f) to be a control
// character but RFC 7159 does not.
// Accept unescaped ASCII NULs in non-strict mode.
auto c = getChar();
if (c < 0x20 && (strict || c != 0))
error("Illegal control character.");
str.put(c);
goto Next;
}
return str.data.length ? str.data : "";
}
bool tryGetSpecialFloat(string str, out double val) {
switch (str)
{
case JSONFloatLiteral.nan:
val = double.nan;
return true;
case JSONFloatLiteral.inf:
val = double.infinity;
return true;
case JSONFloatLiteral.negativeInf:
val = -double.infinity;
return true;
default:
return false;
}
}
void parseValue(ref JSONValue value)
{
depth++;
if (maxDepth != -1 && depth > maxDepth) error("Nesting too deep.");
auto c = getChar!true();
switch (c)
{
case '{':
if (testChar('}'))
{
value.object = null;
break;
}
JSONValue[string] obj;
do
{
checkChar('"');
string name = parseString();
checkChar(':');
JSONValue member;
parseValue(member);
obj[name] = member;
}
while (testChar(','));
value.object = obj;
checkChar('}');
break;
case '[':
if (testChar(']'))
{
value.type_tag = JSON_TYPE.ARRAY;
break;
}
JSONValue[] arr;
do
{
JSONValue element;
parseValue(element);
arr ~= element;
}
while (testChar(','));
checkChar(']');
value.array = arr;
break;
case '"':
auto str = parseString();
// if special float parsing is enabled, check if string represents NaN/Inf
if ((options & JSONOptions.specialFloatLiterals) &&
tryGetSpecialFloat(str, value.store.floating))
{
// found a special float, its value was placed in value.store.floating
value.type_tag = JSON_TYPE.FLOAT;
break;
}
value.type_tag = JSON_TYPE.STRING;
value.store.str = str;
break;
case '0': .. case '9':
case '-':
auto number = appender!string();
bool isFloat, isNegative;
void readInteger()
{
if (!isDigit(c)) error("Digit expected");
Next: number.put(c);
if (isDigit(peekChar()))
{
c = getChar();
goto Next;
}
}
if (c == '-')
{
number.put('-');
c = getChar();
isNegative = true;
}
if (strict && c == '0')
{
number.put('0');
if (isDigit(peekChar()))
{
error("Additional digits not allowed after initial zero digit");
}
}
else
{
readInteger();
}
if (testChar('.'))
{
isFloat = true;
number.put('.');
c = getChar();
readInteger();
}
if (testChar!(false, false)('e'))
{
isFloat = true;
number.put('e');
if (testChar('+')) number.put('+');
else if (testChar('-')) number.put('-');
c = getChar();
readInteger();
}
string data = number.data;
if (isFloat)
{
value.type_tag = JSON_TYPE.FLOAT;
value.store.floating = parse!double(data);
}
else
{
if (isNegative)
value.store.integer = parse!long(data);
else
value.store.uinteger = parse!ulong(data);
value.type_tag = !isNegative && value.store.uinteger & (1UL << 63) ?
JSON_TYPE.UINTEGER : JSON_TYPE.INTEGER;
}
break;
case 'T':
if (strict) goto default;
goto case;
case 't':
value.type_tag = JSON_TYPE.TRUE;
checkChar!false('r', strict);
checkChar!false('u', strict);
checkChar!false('e', strict);
break;
case 'F':
if (strict) goto default;
goto case;
case 'f':
value.type_tag = JSON_TYPE.FALSE;
checkChar!false('a', strict);
checkChar!false('l', strict);
checkChar!false('s', strict);
checkChar!false('e', strict);
break;
case 'N':
if (strict) goto default;
goto case;
case 'n':
value.type_tag = JSON_TYPE.NULL;
checkChar!false('u', strict);
checkChar!false('l', strict);
checkChar!false('l', strict);
break;
default:
error(text("Unexpected character '", c, "'."));
}
depth--;
}
parseValue(root);
if (strict)
{
skipWhitespace();
if (!peekCharNullable().isNull) error("Trailing non-whitespace characters");
}
return root;
}
@safe unittest
{
enum issue15742objectOfObject = `{ "key1": { "key2": 1 }}`;
static assert(parseJSON(issue15742objectOfObject).type == JSON_TYPE.OBJECT);
enum issue15742arrayOfArray = `[[1]]`;
static assert(parseJSON(issue15742arrayOfArray).type == JSON_TYPE.ARRAY);
}
@safe unittest
{
// Ensure we can parse and use JSON from @safe code
auto a = `{ "key1": { "key2": 1 }}`.parseJSON;
assert(a["key1"]["key2"].integer == 1);
assert(a.toString == `{"key1":{"key2":1}}`);
}
@system unittest
{
// Ensure we can parse JSON from a @system range.
struct Range
{
string s;
size_t index;
@system
{
bool empty() { return index >= s.length; }
void popFront() { index++; }
char front() { return s[index]; }
}
}
auto s = Range(`{ "key1": { "key2": 1 }}`);
auto json = parseJSON(s);
assert(json["key1"]["key2"].integer == 1);
}
/**
Parses a serialized string and returns a tree of JSON values.
Throws: $(LREF JSONException) if the depth exceeds the max depth.
Params:
json = json-formatted string to parse
options = enable decoding string representations of NaN/Inf as float values
*/
JSONValue parseJSON(T)(T json, JSONOptions options)
if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T))
{
return parseJSON!T(json, -1, options);
}
/**
Takes a tree of JSON values and returns the serialized string.
Any Object types will be serialized in a key-sorted order.
If `pretty` is false no whitespaces are generated.
If `pretty` is true serialized string is formatted to be human-readable.
Set the $(LREF JSONOptions.specialFloatLiterals) flag is set in `options` to encode NaN/Infinity as strings.
*/
string toJSON(const ref JSONValue root, in bool pretty = false, in JSONOptions options = JSONOptions.none) @safe
{
auto json = appender!string();
void toStringImpl(Char)(string str) @safe
{
json.put('"');
foreach (Char c; str)
{
switch (c)
{
case '"': json.put("\\\""); break;
case '\\': json.put("\\\\"); break;
case '/':
if (!(options & JSONOptions.doNotEscapeSlashes))
json.put('\\');
json.put('/');
break;
case '\b': json.put("\\b"); break;
case '\f': json.put("\\f"); break;
case '\n': json.put("\\n"); break;
case '\r': json.put("\\r"); break;
case '\t': json.put("\\t"); break;
default:
{
import std.ascii : isControl;
import std.utf : encode;
// Make sure we do UTF decoding iff we want to
// escape Unicode characters.
assert(((options & JSONOptions.escapeNonAsciiChars) != 0)
== is(Char == dchar), "JSONOptions.escapeNonAsciiChars needs dchar strings");
with (JSONOptions) if (isControl(c) ||
((options & escapeNonAsciiChars) >= escapeNonAsciiChars && c >= 0x80))
{
// Ensure non-BMP characters are encoded as a pair
// of UTF-16 surrogate characters, as per RFC 4627.
wchar[2] wchars; // 1 or 2 UTF-16 code units
size_t wNum = encode(wchars, c); // number of UTF-16 code units
foreach (wc; wchars[0 .. wNum])
{
json.put("\\u");
foreach_reverse (i; 0 .. 4)
{
char ch = (wc >>> (4 * i)) & 0x0f;
ch += ch < 10 ? '0' : 'A' - 10;
json.put(ch);
}
}
}
else
{
json.put(c);
}
}
}
}
json.put('"');
}
void toString(string str) @safe
{
// Avoid UTF decoding when possible, as it is unnecessary when
// processing JSON.
if (options & JSONOptions.escapeNonAsciiChars)
toStringImpl!dchar(str);
else
toStringImpl!char(str);
}
void toValue(ref in JSONValue value, ulong indentLevel) @safe
{
void putTabs(ulong additionalIndent = 0)
{
if (pretty)
foreach (i; 0 .. indentLevel + additionalIndent)
json.put(" ");
}
void putEOL()
{
if (pretty)
json.put('\n');
}
void putCharAndEOL(char ch)
{
json.put(ch);
putEOL();
}
final switch (value.type)
{
case JSON_TYPE.OBJECT:
auto obj = value.objectNoRef;
if (!obj.length)
{
json.put("{}");
}
else
{
putCharAndEOL('{');
bool first = true;
void emit(R)(R names)
{
foreach (name; names)
{
auto member = obj[name];
if (!first)
putCharAndEOL(',');
first = false;
putTabs(1);
toString(name);
json.put(':');
if (pretty)
json.put(' ');
toValue(member, indentLevel + 1);
}
}
import std.algorithm.sorting : sort;
// @@@BUG@@@ 14439
// auto names = obj.keys; // aa.keys can't be called in @safe code
auto names = new string[obj.length];
size_t i = 0;
foreach (k, v; obj)
{
names[i] = k;
i++;
}
sort(names);
emit(names);
putEOL();
putTabs();
json.put('}');
}
break;
case JSON_TYPE.ARRAY:
auto arr = value.arrayNoRef;
if (arr.empty)
{
json.put("[]");
}
else
{
putCharAndEOL('[');
foreach (i, el; arr)
{
if (i)
putCharAndEOL(',');
putTabs(1);
toValue(el, indentLevel + 1);
}
putEOL();
putTabs();
json.put(']');
}
break;
case JSON_TYPE.STRING:
toString(value.str);
break;
case JSON_TYPE.INTEGER:
json.put(to!string(value.store.integer));
break;
case JSON_TYPE.UINTEGER:
json.put(to!string(value.store.uinteger));
break;
case JSON_TYPE.FLOAT:
import std.math : isNaN, isInfinity;
auto val = value.store.floating;
if (val.isNaN)
{
if (options & JSONOptions.specialFloatLiterals)
{
toString(JSONFloatLiteral.nan);
}
else
{
throw new JSONException(
"Cannot encode NaN. Consider passing the specialFloatLiterals flag.");
}
}
else if (val.isInfinity)
{
if (options & JSONOptions.specialFloatLiterals)
{
toString((val > 0) ? JSONFloatLiteral.inf : JSONFloatLiteral.negativeInf);
}
else
{
throw new JSONException(
"Cannot encode Infinity. Consider passing the specialFloatLiterals flag.");
}
}
else
{
import std.format : format;
// The correct formula for the number of decimal digits needed for lossless round
// trips is actually:
// ceil(log(pow(2.0, double.mant_dig - 1)) / log(10.0) + 1) == (double.dig + 2)
// Anything less will round off (1 + double.epsilon)
json.put("%.18g".format(val));
}
break;
case JSON_TYPE.TRUE:
json.put("true");
break;
case JSON_TYPE.FALSE:
json.put("false");
break;
case JSON_TYPE.NULL:
json.put("null");
break;
}
}
toValue(root, 0);
return json.data;
}
@safe unittest // bugzilla 12897
{
JSONValue jv0 = JSONValue("test测试");
assert(toJSON(jv0, false, JSONOptions.escapeNonAsciiChars) == `"test\u6D4B\u8BD5"`);
JSONValue jv00 = JSONValue("test\u6D4B\u8BD5");
assert(toJSON(jv00, false, JSONOptions.none) == `"test测试"`);
assert(toJSON(jv0, false, JSONOptions.none) == `"test测试"`);
JSONValue jv1 = JSONValue("été");
assert(toJSON(jv1, false, JSONOptions.escapeNonAsciiChars) == `"\u00E9t\u00E9"`);
JSONValue jv11 = JSONValue("\u00E9t\u00E9");
assert(toJSON(jv11, false, JSONOptions.none) == `"été"`);
assert(toJSON(jv1, false, JSONOptions.none) == `"été"`);
}
/**
Exception thrown on JSON errors
*/
class JSONException : Exception
{
this(string msg, int line = 0, int pos = 0) pure nothrow @safe
{
if (line)
super(text(msg, " (Line ", line, ":", pos, ")"));
else
super(msg);
}
this(string msg, string file, size_t line) pure nothrow @safe
{
super(msg, file, line);
}
}
@system unittest
{
import std.exception;
JSONValue jv = "123";
assert(jv.type == JSON_TYPE.STRING);
assertNotThrown(jv.str);
assertThrown!JSONException(jv.integer);
assertThrown!JSONException(jv.uinteger);
assertThrown!JSONException(jv.floating);
assertThrown!JSONException(jv.object);
assertThrown!JSONException(jv.array);
assertThrown!JSONException(jv["aa"]);
assertThrown!JSONException(jv[2]);
jv = -3;
assert(jv.type == JSON_TYPE.INTEGER);
assertNotThrown(jv.integer);
jv = cast(uint) 3;
assert(jv.type == JSON_TYPE.UINTEGER);
assertNotThrown(jv.uinteger);
jv = 3.0;
assert(jv.type == JSON_TYPE.FLOAT);
assertNotThrown(jv.floating);
jv = ["key" : "value"];
assert(jv.type == JSON_TYPE.OBJECT);
assertNotThrown(jv.object);
assertNotThrown(jv["key"]);
assert("key" in jv);
assert("notAnElement" !in jv);
assertThrown!JSONException(jv["notAnElement"]);
const cjv = jv;
assert("key" in cjv);
assertThrown!JSONException(cjv["notAnElement"]);
foreach (string key, value; jv)
{
static assert(is(typeof(value) == JSONValue));
assert(key == "key");
assert(value.type == JSON_TYPE.STRING);
assertNotThrown(value.str);
assert(value.str == "value");
}
jv = [3, 4, 5];
assert(jv.type == JSON_TYPE.ARRAY);
assertNotThrown(jv.array);
assertNotThrown(jv[2]);
foreach (size_t index, value; jv)
{
static assert(is(typeof(value) == JSONValue));
assert(value.type == JSON_TYPE.INTEGER);
assertNotThrown(value.integer);
assert(index == (value.integer-3));
}
jv = null;
assert(jv.type == JSON_TYPE.NULL);
assert(jv.isNull);
jv = "foo";
assert(!jv.isNull);
jv = JSONValue("value");
assert(jv.type == JSON_TYPE.STRING);
assert(jv.str == "value");
JSONValue jv2 = JSONValue("value");
assert(jv2.type == JSON_TYPE.STRING);
assert(jv2.str == "value");
JSONValue jv3 = JSONValue("\u001c");
assert(jv3.type == JSON_TYPE.STRING);
assert(jv3.str == "\u001C");
}
@system unittest
{
// Bugzilla 11504
JSONValue jv = 1;
assert(jv.type == JSON_TYPE.INTEGER);
jv.str = "123";
assert(jv.type == JSON_TYPE.STRING);
assert(jv.str == "123");
jv.integer = 1;
assert(jv.type == JSON_TYPE.INTEGER);
assert(jv.integer == 1);
jv.uinteger = 2u;
assert(jv.type == JSON_TYPE.UINTEGER);
assert(jv.uinteger == 2u);
jv.floating = 1.5;
assert(jv.type == JSON_TYPE.FLOAT);
assert(jv.floating == 1.5);
jv.object = ["key" : JSONValue("value")];
assert(jv.type == JSON_TYPE.OBJECT);
assert(jv.object == ["key" : JSONValue("value")]);
jv.array = [JSONValue(1), JSONValue(2), JSONValue(3)];
assert(jv.type == JSON_TYPE.ARRAY);
assert(jv.array == [JSONValue(1), JSONValue(2), JSONValue(3)]);
jv = true;
assert(jv.type == JSON_TYPE.TRUE);
jv = false;
assert(jv.type == JSON_TYPE.FALSE);
enum E{True = true}
jv = E.True;
assert(jv.type == JSON_TYPE.TRUE);
}
@system pure unittest
{
// Adding new json element via array() / object() directly
JSONValue jarr = JSONValue([10]);
foreach (i; 0 .. 9)
jarr.array ~= JSONValue(i);
assert(jarr.array.length == 10);
JSONValue jobj = JSONValue(["key" : JSONValue("value")]);
foreach (i; 0 .. 9)
jobj.object[text("key", i)] = JSONValue(text("value", i));
assert(jobj.object.length == 10);
}
@system pure unittest
{
// Adding new json element without array() / object() access
JSONValue jarr = JSONValue([10]);
foreach (i; 0 .. 9)
jarr ~= [JSONValue(i)];
assert(jarr.array.length == 10);
JSONValue jobj = JSONValue(["key" : JSONValue("value")]);
foreach (i; 0 .. 9)
jobj[text("key", i)] = JSONValue(text("value", i));
assert(jobj.object.length == 10);
// No array alias
auto jarr2 = jarr ~ [1,2,3];
jarr2[0] = 999;
assert(jarr[0] == JSONValue(10));
}
@system unittest
{
// @system because JSONValue.array is @system
import std.exception;
// An overly simple test suite, if it can parse a serializated string and
// then use the resulting values tree to generate an identical
// serialization, both the decoder and encoder works.
auto jsons = [
`null`,
`true`,
`false`,
`0`,
`123`,
`-4321`,
`0.25`,
`-0.25`,
`""`,
`"hello\nworld"`,
`"\"\\\/\b\f\n\r\t"`,
`[]`,
`[12,"foo",true,false]`,
`{}`,
`{"a":1,"b":null}`,
`{"goodbye":[true,"or",false,["test",42,{"nested":{"a":23.5,"b":0.140625}}]],`
~`"hello":{"array":[12,null,{}],"json":"is great"}}`,
];
enum dbl1_844 = `1.8446744073709568`;
version (MinGW)
jsons ~= dbl1_844 ~ `e+019`;
else
jsons ~= dbl1_844 ~ `e+19`;
JSONValue val;
string result;
foreach (json; jsons)
{
try
{
val = parseJSON(json);
enum pretty = false;
result = toJSON(val, pretty);
assert(result == json, text(result, " should be ", json));
}
catch (JSONException e)
{
import std.stdio : writefln;
writefln(text(json, "\n", e.toString()));
}
}
// Should be able to correctly interpret unicode entities
val = parseJSON(`"\u003C\u003E"`);
assert(toJSON(val) == "\"\&lt;\&gt;\"");
assert(val.to!string() == "\"\&lt;\&gt;\"");
val = parseJSON(`"\u0391\u0392\u0393"`);
assert(toJSON(val) == "\"\&Alpha;\&Beta;\&Gamma;\"");
assert(val.to!string() == "\"\&Alpha;\&Beta;\&Gamma;\"");
val = parseJSON(`"\u2660\u2666"`);
assert(toJSON(val) == "\"\&spades;\&diams;\"");
assert(val.to!string() == "\"\&spades;\&diams;\"");
//0x7F is a control character (see Unicode spec)
val = parseJSON(`"\u007F"`);
assert(toJSON(val) == "\"\\u007F\"");
assert(val.to!string() == "\"\\u007F\"");
with(parseJSON(`""`))
assert(str == "" && str !is null);
with(parseJSON(`[]`))
assert(!array.length);
// Formatting
val = parseJSON(`{"a":[null,{"x":1},{},[]]}`);
assert(toJSON(val, true) == `{
"a": [
null,
{
"x": 1
},
{},
[]
]
}`);
}
@safe unittest
{
auto json = `"hello\nworld"`;
const jv = parseJSON(json);
assert(jv.toString == json);
assert(jv.toPrettyString == json);
}
@system pure unittest
{
// Bugzilla 12969
JSONValue jv;
jv["int"] = 123;
assert(jv.type == JSON_TYPE.OBJECT);
assert("int" in jv);
assert(jv["int"].integer == 123);
jv["array"] = [1, 2, 3, 4, 5];
assert(jv["array"].type == JSON_TYPE.ARRAY);
assert(jv["array"][2].integer == 3);
jv["str"] = "D language";
assert(jv["str"].type == JSON_TYPE.STRING);
assert(jv["str"].str == "D language");
jv["bool"] = false;
assert(jv["bool"].type == JSON_TYPE.FALSE);
assert(jv.object.length == 4);
jv = [5, 4, 3, 2, 1];
assert( jv.type == JSON_TYPE.ARRAY );
assert( jv[3].integer == 2 );
}
@safe unittest
{
auto s = q"EOF
[
1,
2,
3,
potato
]
EOF";
import std.exception;
auto e = collectException!JSONException(parseJSON(s));
assert(e.msg == "Unexpected character 'p'. (Line 5:3)", e.msg);
}
// handling of special float values (NaN, Inf, -Inf)
@safe unittest
{
import std.exception : assertThrown;
import std.math : isNaN, isInfinity;
// expected representations of NaN and Inf
enum {
nanString = '"' ~ JSONFloatLiteral.nan ~ '"',
infString = '"' ~ JSONFloatLiteral.inf ~ '"',
negativeInfString = '"' ~ JSONFloatLiteral.negativeInf ~ '"',
}
// with the specialFloatLiterals option, encode NaN/Inf as strings
assert(JSONValue(float.nan).toString(JSONOptions.specialFloatLiterals) == nanString);
assert(JSONValue(double.infinity).toString(JSONOptions.specialFloatLiterals) == infString);
assert(JSONValue(-real.infinity).toString(JSONOptions.specialFloatLiterals) == negativeInfString);
// without the specialFloatLiterals option, throw on encoding NaN/Inf
assertThrown!JSONException(JSONValue(float.nan).toString);
assertThrown!JSONException(JSONValue(double.infinity).toString);
assertThrown!JSONException(JSONValue(-real.infinity).toString);
// when parsing json with specialFloatLiterals option, decode special strings as floats
JSONValue jvNan = parseJSON(nanString, JSONOptions.specialFloatLiterals);
JSONValue jvInf = parseJSON(infString, JSONOptions.specialFloatLiterals);
JSONValue jvNegInf = parseJSON(negativeInfString, JSONOptions.specialFloatLiterals);
assert(jvNan.floating.isNaN);
assert(jvInf.floating.isInfinity && jvInf.floating > 0);
assert(jvNegInf.floating.isInfinity && jvNegInf.floating < 0);
// when parsing json without the specialFloatLiterals option, decode special strings as strings
jvNan = parseJSON(nanString);
jvInf = parseJSON(infString);
jvNegInf = parseJSON(negativeInfString);
assert(jvNan.str == JSONFloatLiteral.nan);
assert(jvInf.str == JSONFloatLiteral.inf);
assert(jvNegInf.str == JSONFloatLiteral.negativeInf);
}
pure nothrow @safe @nogc unittest
{
JSONValue testVal;
testVal = "test";
testVal = 10;
testVal = 10u;
testVal = 1.0;
testVal = (JSONValue[string]).init;
testVal = JSONValue[].init;
testVal = null;
assert(testVal.isNull);
}
pure nothrow @safe unittest // issue 15884
{
import std.typecons;
void Test(C)() {
C[] a = ['x'];
JSONValue testVal = a;
assert(testVal.type == JSON_TYPE.STRING);
testVal = a.idup;
assert(testVal.type == JSON_TYPE.STRING);
}
Test!char();
Test!wchar();
Test!dchar();
}
@safe unittest // issue 15885
{
enum bool realInDoublePrecision = real.mant_dig == double.mant_dig;
static bool test(const double num0)
{
import std.math : feqrel;
const json0 = JSONValue(num0);
const num1 = to!double(toJSON(json0));
static if (realInDoublePrecision)
return feqrel(num1, num0) >= (double.mant_dig - 1);
else
return num1 == num0;
}
assert(test( 0.23));
assert(test(-0.23));
assert(test(1.223e+24));
assert(test(23.4));
assert(test(0.0012));
assert(test(30738.22));
assert(test(1 + double.epsilon));
assert(test(double.min_normal));
static if (realInDoublePrecision)
assert(test(-double.max / 2));
else
assert(test(-double.max));
const minSub = double.min_normal * double.epsilon;
assert(test(minSub));
assert(test(3*minSub));
}
@safe unittest // issue 17555
{
import std.exception : assertThrown;
assertThrown!JSONException(parseJSON("\"a\nb\""));
}
@safe unittest // issue 17556
{
auto v = JSONValue("\U0001D11E");
auto j = toJSON(v, false, JSONOptions.escapeNonAsciiChars);
assert(j == `"\uD834\uDD1E"`);
}
@safe unittest // issue 5904
{
string s = `"\uD834\uDD1E"`;
auto j = parseJSON(s);
assert(j.str == "\U0001D11E");
}
@safe unittest // issue 17557
{
assert(parseJSON("\"\xFF\"").str == "\xFF");
assert(parseJSON("\"\U0001D11E\"").str == "\U0001D11E");
}
@safe unittest // issue 17553
{
auto v = JSONValue("\xFF");
assert(toJSON(v) == "\"\xFF\"");
}
@safe unittest
{
import std.utf;
assert(parseJSON("\"\xFF\"".byChar).str == "\xFF");
assert(parseJSON("\"\U0001D11E\"".byChar).str == "\U0001D11E");
}
@safe unittest // JSONOptions.doNotEscapeSlashes (issue 17587)
{
assert(parseJSON(`"/"`).toString == `"\/"`);
assert(parseJSON(`"\/"`).toString == `"\/"`);
assert(parseJSON(`"/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`);
assert(parseJSON(`"\/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`);
}
@safe unittest // JSONOptions.strictParsing (issue 16639)
{
import std.exception : assertThrown;
// Unescaped ASCII NULs
assert(parseJSON("[\0]").type == JSON_TYPE.ARRAY);
assertThrown!JSONException(parseJSON("[\0]", JSONOptions.strictParsing));
assert(parseJSON("\"\0\"").str == "\0");
assertThrown!JSONException(parseJSON("\"\0\"", JSONOptions.strictParsing));
// Unescaped ASCII DEL (0x7f) in strings
assert(parseJSON("\"\x7f\"").str == "\x7f");
assert(parseJSON("\"\x7f\"", JSONOptions.strictParsing).str == "\x7f");
// "true", "false", "null" case sensitivity
assert(parseJSON("true").type == JSON_TYPE.TRUE);
assert(parseJSON("true", JSONOptions.strictParsing).type == JSON_TYPE.TRUE);
assert(parseJSON("True").type == JSON_TYPE.TRUE);
assertThrown!JSONException(parseJSON("True", JSONOptions.strictParsing));
assert(parseJSON("tRUE").type == JSON_TYPE.TRUE);
assertThrown!JSONException(parseJSON("tRUE", JSONOptions.strictParsing));
assert(parseJSON("false").type == JSON_TYPE.FALSE);
assert(parseJSON("false", JSONOptions.strictParsing).type == JSON_TYPE.FALSE);
assert(parseJSON("False").type == JSON_TYPE.FALSE);
assertThrown!JSONException(parseJSON("False", JSONOptions.strictParsing));
assert(parseJSON("fALSE").type == JSON_TYPE.FALSE);
assertThrown!JSONException(parseJSON("fALSE", JSONOptions.strictParsing));
assert(parseJSON("null").type == JSON_TYPE.NULL);
assert(parseJSON("null", JSONOptions.strictParsing).type == JSON_TYPE.NULL);
assert(parseJSON("Null").type == JSON_TYPE.NULL);
assertThrown!JSONException(parseJSON("Null", JSONOptions.strictParsing));
assert(parseJSON("nULL").type == JSON_TYPE.NULL);
assertThrown!JSONException(parseJSON("nULL", JSONOptions.strictParsing));
// Whitespace characters
assert(parseJSON("[\f\v]").type == JSON_TYPE.ARRAY);
assertThrown!JSONException(parseJSON("[\f\v]", JSONOptions.strictParsing));
assert(parseJSON("[ \t\r\n]").type == JSON_TYPE.ARRAY);
assert(parseJSON("[ \t\r\n]", JSONOptions.strictParsing).type == JSON_TYPE.ARRAY);
// Empty input
assert(parseJSON("").type == JSON_TYPE.NULL);
assertThrown!JSONException(parseJSON("", JSONOptions.strictParsing));
// Numbers with leading '0's
assert(parseJSON("01").integer == 1);
assertThrown!JSONException(parseJSON("01", JSONOptions.strictParsing));
assert(parseJSON("-01").integer == -1);
assertThrown!JSONException(parseJSON("-01", JSONOptions.strictParsing));
assert(parseJSON("0.01").floating == 0.01);
assert(parseJSON("0.01", JSONOptions.strictParsing).floating == 0.01);
assert(parseJSON("0e1").floating == 0);
assert(parseJSON("0e1", JSONOptions.strictParsing).floating == 0);
// Trailing characters after JSON value
assert(parseJSON(`""asdf`).str == "");
assertThrown!JSONException(parseJSON(`""asdf`, JSONOptions.strictParsing));
assert(parseJSON("987\0").integer == 987);
assertThrown!JSONException(parseJSON("987\0", JSONOptions.strictParsing));
assert(parseJSON("987\0\0").integer == 987);
assertThrown!JSONException(parseJSON("987\0\0", JSONOptions.strictParsing));
assert(parseJSON("[]]").type == JSON_TYPE.ARRAY);
assertThrown!JSONException(parseJSON("[]]", JSONOptions.strictParsing));
assert(parseJSON("123 \t\r\n").integer == 123); // Trailing whitespace is OK
assert(parseJSON("123 \t\r\n", JSONOptions.strictParsing).integer == 123);
}