Compare commits

..
Author SHA1 Message Date
Niels Lohmann 65fbbc5588 Fix GCC and clang-tidy warnings in the JSON pointer speedups
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:05 +02:00
Niels Lohmann 8c2857d974 Declare the partner iterator in the loop initializer in diff_object_keys()
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:04 +02:00
Niels Lohmann 007aaaf113 Use std::array for the index buffer in append_index()
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:03 +02:00
Niels Lohmann 8d61fb8ca2 Diff objects with the same keys in lockstep and keep one path buffer
diff() looked up every key of the source object in the target object,
and built a new string for the path of every value it descended into.
Objects of the same size usually have the same keys in the same order,
so they are now walked together as long as that holds, without a
lookup; objects that differ fall back to the lookup from the first key
that does not match. The path is one buffer that is extended before
descending and shortened again afterwards. Objects whose members are
equal but in a different order (possible with ordered_json) still get
no operations.

Together with the previous commit, diff() of canada.json,
citm_catalog.json and twitter.json is 1.5x to 2.0x as fast; ordered_json
1.65x to 1.85x. The output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:02 +02:00
Niels Lohmann 278a73bd20 Build diff operations member by member
diff() created every operation from an initializer list, so each
{"op", "replace"} pair became a temporary array with two heap strings
before it was turned into an object member; an operation took about 25
allocations. The operations are now built as objects whose members are
assigned one by one. Paths of members and elements are also built in
place, without a temporary string for each escaped key or array index.

diff() of canada.json, citm_catalog.json and twitter.json, with every
eighth scalar changed, is 1.3x to 1.6x as fast; the output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:01 +02:00
Niels Lohmann ba9448e007 Apply plain JSON Patch operations without building a json_pointer
patch() built a json_pointer for every operation, which splits the path
into a vector of strings, and for "add" and "remove" a second one for
the parent. That took 20% to 45% of the time not spent copying the
document.

"add", "remove", "replace" and "test" with a path that has no escapes and
denotes existing values (the parent, for "add"), as diff() creates them,
are now applied by resolving the path in place, reusing one string for
the reference tokens. Every other operation, and every operation that
would fail, takes the existing code, so the results, the exceptions and
the state left after a failing operation are unchanged.

patch() of the diffs of canada.json, citm_catalog.json and twitter.json
is 1.02x to 1.10x as fast; the rest is the copy of the document.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:01 +02:00
Niels Lohmann 6baf56a239 Unflatten without splitting keys or walking each path from the root
unflatten() split every key into a vector of reference tokens, stored the
pointer prefixes in a tree to decide which values become arrays, and then
looked up every token of every key from the root again.

Keys are now checked by a scan that raises the same parse_error.107/108 as
split(), skipping the part a key shares with the previous one, which was
checked already. The prefixes that denote arrays are kept in a small hash
set of key slices; the array/object decision is unchanged. The walk keeps
the values along the previous key's path, so only the part of a key that
differs from the previous one is looked up. Keys of a flattened object are
usually sorted, so that part is short.

unflatten() of canada.json, citm_catalog.json and twitter.json is 1.9x to
2.3x as fast; the output and the exceptions are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:29:00 +02:00
Niels Lohmann f3ae7f618e Flatten into a std::map by inserting next to the previous entry
flatten() added every path with operator[], so each insertion into the
std::map of the result searched the tree from the root and compared long
shared path prefixes, which took half the time on canada.json. Paths come
out in nearly sorted order, so they are now inserted with emplace_hint
next to the entry added last, for object types that have it. ordered_map
keeps its append without lookup, and other object types keep
operator[].

Path segments are also appended in place, without a temporary string
for each escaped key or array index.

flatten() of canada.json, citm_catalog.json and twitter.json is 1.4x to
1.9x as fast; ordered_json is unchanged (1.05x). The output is
unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-17 08:28:59 +02:00
37 changed files with 1567 additions and 7177 deletions

No files matched your search

-2
View File
@@ -111,8 +111,6 @@ context which existing file needs to be extended, and only very few cases requir
When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue. When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue.
`unit-regression2.cpp` holds the older tests; the two files exist because a single one grew large enough for the `unit-regression2.cpp` holds the older tests; the two files exist because a single one grew large enough for the
MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing the larger one. MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing the larger one.
Tests that call `sax_parse` go into `unit-sax_parse.cpp` rather than into a large test file: every call instantiates
the parser and the binary reader that recover from errors, which grows a test file considerably.
#### Exceptions #### Exceptions
+2 -2
View File
@@ -495,7 +495,7 @@ bool key(string_t& val);
bool parse_error(std::size_t position, const std::string& last_token, const detail::exception& ex); bool parse_error(std::size_t position, const std::string& last_token, const detail::exception& ex);
``` ```
The return value of each function determines whether parsing should proceed. For `parse_error`, returning `true` [recovers from the error](https://json.nlohmann.me/features/parsing/error_recovery/): the parser repairs the input and continues. The return value of each function determines whether parsing should proceed.
To implement your own SAX handler, proceed as follows: To implement your own SAX handler, proceed as follows:
@@ -503,7 +503,7 @@ To implement your own SAX handler, proceed as follows:
2. Create an object of your SAX interface class, e.g. `my_sax`. 2. Create an object of your SAX interface class, e.g. `my_sax`.
3. Call `bool json::sax_parse(input, &my_sax)`; where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface. 3. Call `bool json::sax_parse(input, &my_sax)`; where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface.
Note the `sax_parse` function only returns a `bool` indicating whether the input was parsed without errors and no SAX event returned `false`. It does not return a `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error -- it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file [`json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp). Note the `sax_parse` function only returns a `bool` indicating the result of the last executed SAX event. It does not return a `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error -- it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file [`json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp).
### STL-like access ### STL-like access
+1 -4
View File
@@ -96,9 +96,7 @@ The SAX event lister must follow the interface of [`json_sax`](../json_sax/index
## Return value ## Return value
`#!cpp true` if the input was parsed without errors and no SAX event returned `#!cpp false`; `#!cpp false` otherwise. return value of the last processed SAX event
In particular, the result is `#!cpp false` for input with errors, even if the SAX parser recovered from all of them
(see [error recovery](../../features/parsing/error_recovery.md)).
## Exception safety ## Exception safety
@@ -147,7 +145,6 @@ A UTF-8 byte order mark is silently ignored.
- Added `ignore_trailing_commas` in version 3.13.0. - Added `ignore_trailing_commas` in version 3.13.0.
- Added `tag_handler` in version 3.13.0. - Added `tag_handler` in version 3.13.0.
- Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0. - Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0.
- Recovering from parse errors (see [`parse_error`](../json_sax/parse_error.md)) added in version 3.13.0.
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0. - Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right - `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right
after the parsed value when `strict` is `#!cpp false`. after the parsed value when `strict` is `#!cpp false`.
+1 -2
View File
@@ -7,8 +7,7 @@ struct json_sax;
This class describes the SAX interface used by [sax_parse](../basic_json/sax_parse.md). Each function is called in This class describes the SAX interface used by [sax_parse](../basic_json/sax_parse.md). Each function is called in
different situations while the input is parsed. The boolean return value informs the parser whether to continue different situations while the input is parsed. The boolean return value informs the parser whether to continue
processing the input; for [`parse_error`](parse_error.md), it decides whether to processing the input.
[recover from the error](../../features/parsing/error_recovery.md).
For instance, parsing the JSON text `{"a": [1, true]}` triggers the following callbacks, in order: For instance, parsing the JSON text `{"a": [1, true]}` triggers the following callbacks, in order:
+2 -26
View File
@@ -21,18 +21,11 @@ A parse error occurred.
## Return value ## Return value
Whether to recover from the error: Whether parsing should proceed (**must return `#!cpp false`**).
- `#!cpp false` stops parsing.
- `#!cpp true` recovers from the error: the error is repaired and parsing continues. If that is not possible, which
happens in the binary formats when the end of the item with the error is unknown, the value read so far is completed
and parsing stops. See [error recovery](../../features/parsing/error_recovery.md) for how errors are repaired.
Either way, [`sax_parse`](../basic_json/sax_parse.md) returns `#!cpp false`.
## Examples ## Examples
??? example "Example: (1) the SAX interface" ??? example
The example below shows how the SAX interface is used. The example below shows how the SAX interface is used.
@@ -46,29 +39,12 @@ Either way, [`sax_parse`](../basic_json/sax_parse.md) returns `#!cpp false`.
--8<-- "examples/sax_parse.output" --8<-- "examples/sax_parse.output"
``` ```
??? example "Example: (2) recovering from errors"
The example below shows how a SAX parser recovers from errors.
```cpp
--8<-- "examples/sax_parse__error_recovery.cpp"
```
Output:
```
--8<-- "examples/sax_parse__error_recovery.output"
```
## See also ## See also
- [sax_parse](../basic_json/sax_parse.md) - SAX parser - [sax_parse](../basic_json/sax_parse.md) - SAX parser
- [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - the article on handling parse errors without - [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - the article on handling parse errors without
exceptions exceptions
- [Error Recovery](../../features/parsing/error_recovery.md) - the article on recovering from parse errors
## Version history ## Version history
- Added in version 3.2.0. - Added in version 3.2.0.
- Returning `#!cpp true` recovers from the error since version 3.13.0; before, parsing stopped, but the result of
[`sax_parse`](../basic_json/sax_parse.md) could be wrong.
@@ -1,43 +0,0 @@
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// a SAX parser that creates a JSON value like json::parse does, but that
// recovers from parse errors instead of stopping at the first one
class recovering_parser : public nlohmann::detail::json_sax_dom_parser<json>
{
public:
explicit recovering_parser(json& result)
: nlohmann::detail::json_sax_dom_parser<json>(result, false)
{}
bool parse_error(std::size_t position,
const std::string& /*last_token*/,
const json::exception& ex)
{
std::cout << "byte " << position << ": " << ex.what() << '\n';
// repair the input and continue
return true;
}
};
int main()
{
// JSON text with several mistakes that ends too early
const std::string text = R"({
"name": "Hello World",
"tags": ["a" "b",],
"valid": tru,
"size": 1.,
"nested": {"x": 1)";
json result;
recovering_parser sax(result);
const bool valid = json::sax_parse(text, &sax);
std::cout << "\nvalid JSON: " << std::boolalpha << valid << '\n'
<< std::setw(4) << result << std::endl;
}
@@ -1,19 +0,0 @@
byte 49: [json.exception.parse_error.101] parse error at line 3, column 20: syntax error while parsing array - unexpected string literal; expected ']'
byte 51: [json.exception.parse_error.101] parse error at line 3, column 22: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal
byte 70: [json.exception.parse_error.101] parse error at line 4, column 17: syntax error while parsing value - invalid literal; last read: '"valid": tru,'
byte 86: [json.exception.parse_error.101] parse error at line 5, column 15: syntax error while parsing value - invalid number; expected digit after '.'; last read: '1.,'
byte 109: [json.exception.parse_error.101] parse error at line 6, column 22: syntax error while parsing object - unexpected end of input; expected '}'
valid JSON: false
{
"name": "Hello World",
"nested": {
"x": 1
},
"size": 1,
"tags": [
"a",
"b"
],
"valid": null
}
@@ -1,122 +0,0 @@
# Error Recovery
By default, parsing stops at the first error. With the [SAX interface](sax_interface.md), you can instead ask the
parser to *recover*: to repair the error and continue, so that you get as much as possible out of malformed input, for
instance a file that was cut off, JSON edited by hand, or the output of a language model.
## Recovering from errors
The SAX parser's [`parse_error`](../../api/json_sax/parse_error.md) function is called for every error. Its return value
decides what happens next:
- `#!cpp false` stops parsing. This is what the SAX parsers of the library do, so [`parse`](../../api/basic_json/parse.md)
and [`accept`](../../api/basic_json/accept.md) never recover.
- `#!cpp true` repairs the error and continues parsing.
When recovering, the SAX parser still receives well-formed events: every `start_object` or `start_array` is followed by
the matching `end_object` or `end_array`, and every `key` is followed by exactly one value. A SAX parser that creates a
JSON value, such as the one in the example below, therefore gets a complete value. Parsing always ends, and
[`sax_parse`](../../api/basic_json/sax_parse.md) returns `#!cpp false` for input that is not valid JSON, even if every
error was repaired. Each token is reported at most once, and the SAX parser can stop at any error by returning
`#!cpp false`.
!!! example
The example below derives a SAX parser from the library's parser for `json` values (`json_sax_dom_parser`),
and recovers from all errors.
```cpp
--8<-- "examples/sax_parse__error_recovery.cpp"
```
Output:
```
--8<-- "examples/sax_parse__error_recovery.output"
```
## How errors are repaired
Each error is repaired with the smallest local edit: a missing separator is inserted, a stray token is removed, what can
be read of a broken string or number is kept, and a value that cannot be read at all becomes `#!json null`.
| Mistake | Repair | Example | Result |
|---------------------------|--------------------------------------------------------------------------------|------------------------------------------|----------------------------|
| missing `,` or `:` | inserted | `#!json [1 2]`, `#!json {"a" 1}` | `[1,2]`, `{"a":1}` |
| missing value | `#!json null` for an object key or between commas in an array | `#!json {"a":}`, `#!json [1,,2]` | `{"a":null}`, `[1,null,2]` |
| trailing comma | removed | `#!json [1,2,]` | `[1,2]` |
| broken string | invalid escapes and bytes are replaced (see below); a line break ends the string | `#!json ["a\qb"]` | `["aqb"]` |
| broken number | the longest valid beginning is kept | `#!json [1., 2e+]` | `[1,2]` |
| unreadable value | `#!json null` | `#!json [1, NaN, tru]` | `[1,null,null]` |
| number too large | passed as infinity, together with its text | `#!json [1e999]` | infinity (see below) |
| stray `:` | removed | `#!json ["a":1]` | `["a",1]` |
| member without a key | skipped up to the next `,` or `}` | `#!json {1:2, "b":3}` | `{"b":3}` |
| wrong closing bracket | closes the innermost array or object | `#!json {"a":[1,2}, "b":3}` | `{"a":[1,2],"b":3}` |
| input ends too early | all open arrays and objects are closed | `#!json {"a":[1,2` | `{"a":[1,2]}` |
| text before the value | skipped | `#!json )]}'{"a":1}` | `{"a":1}` |
In a string, an unknown escape like `\q` stands for the escaped character (`q`), as in JavaScript. An invalid `\u`
escape, a lone surrogate, and ill-formed UTF-8 are each replaced by U+FFFD (REPLACEMENT CHARACTER), and control
characters are kept. A string without its closing quote ends at the next line break or at the end of the input.
The input after the top-level value is not repaired: as without recovery, it is reported as an error, and parsing stops.
## Binary formats
The binary formats ([BJData](../binary_formats/bjdata.md), [BON8](../binary_formats/bon8.md),
[BSON](../binary_formats/bson.md), [CBOR](../binary_formats/cbor.md), [MessagePack](../binary_formats/messagepack.md),
and [UBJSON](../binary_formats/ubjson.md)) have no delimiters to find the next value by. So what can be repaired depends
on whether the end of the item with the error is known, a distinction that
[RFC 8949, Section 5.3](https://www.rfc-editor.org/rfc/rfc8949.html#section-5.3) makes for CBOR, too.
If the item is complete, but cannot be passed on as it is, it is replaced, and parsing continues after it:
| Mistake | Formats | Repair |
|---------------------------------------------------------------------|-------------------------|-------------------------------------------------------------------------|
| tag, if `tag_handler` is `cbor_tag_handler_t::error` (the default) | CBOR | ignored |
| simple value other than `false`, `true`, and `null`, like undefined | CBOR | `#!json null` |
| number too large for a custom `number_float_t`, like `float` | all | infinity |
| character (`C`) that is not ASCII | BJData, UBJSON | U+FFFD |
| invalid high-precision number (`H`) | BJData, UBJSON | the longest valid beginning is kept, as for JSON text, or `#!json null` |
| high-precision number too large | BJData, UBJSON | passed as infinity, together with its text |
| object key that is not a string | BON8, CBOR, MessagePack | the member is skipped |
| element of a type the library does not read, like ObjectId or date | BSON | `#!json null` |
| string without its terminator | BSON | kept |
| document whose size does not match its content | BSON | kept |
CBOR tags and simple values are repaired as [RFC 8949, Section 6.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-6.1)
suggests for converting CBOR to JSON. By default, [`sax_parse`](../../api/basic_json/sax_parse.md) reports every tag as
an error; when recovering, tags are then ignored like with
[`cbor_tag_handler_t::ignore`](../../api/basic_json/cbor_tag_handler_t.md). Strings that are not valid UTF-8 are no
error: like [`from_cbor`](../../api/basic_json/from_cbor.md) and the other functions by default, `sax_parse` passes
them on as they are.
After any other error, the end of the item is unknown: the input ended, a byte is not a valid type marker, or a size
cannot be right. Parsing then stops, and the value read so far is completed: a key that waits for its value gets
`#!json null`, and all open arrays and objects are closed. This keeps everything before the error of an input that was
cut off. The exception is BSON, which stores the size of every document: an element whose end is unknown gets
`#!json null`, the rest of its document is skipped, and parsing continues after the document.
## Limitations
- A repair is a guess. For example, `#!json {"a" "b": 1}` could be meant as `#!json {"a": "b"}` or as
`#!json {"a": null, "b": 1}`; it is repaired to the former. Treat recovered values as a best effort, and check the
reported errors.
- A closing bracket always closes the innermost array or object. If a bracket is missing rather than wrong, the
repair differs from the intention: `#!json {"a": {"b": [1, 2}, "c": 3}` is repaired to
`#!json {"a": {"b": [1, 2], "c": 3}}`, although `#!json {"a": {"b": [1, 2]}, "c": 3}` may have been meant.
- Keys without quotes, and strings in single quotes, are not supported; such members are skipped.
- In the binary formats, a member that is skipped because its key is not a string is lost, and so are the elements of a
BSON document after one whose end is unknown.
- A number that is too large for `number_float_t` is passed as positive or negative infinity. The SAX parser's
`number_float` also gets the number's text, but a JSON value cannot store it, and
[`dump`](../../api/basic_json/dump.md) serializes infinity as `#!json null`.
- When parsing is not strict (see [`sax_parse`](../../api/basic_json/sax_parse.md)), a repair may read parts of the
input after the value, for instance of the next value in a stream of concatenated values.
## See also
- [SAX interface](sax_interface.md) - implement a custom SAX handler
- [`parse_error`](../../api/json_sax/parse_error.md) - the SAX event for parse errors
- [`sax_parse`](../../api/basic_json/sax_parse.md) - generate SAX events
- [parsing and exceptions](parse_exceptions.md) - control error handling
+1 -2
View File
@@ -75,7 +75,7 @@ You can influence a DOM parse without switching to the SAX interface by passing
When the input is not valid JSON, the `parse` function throws an exception by default. If exceptions are undesired or When the input is not valid JSON, the `parse` function throws an exception by default. If exceptions are undesired or
unavailable, the parser can instead return a discarded value, or [`accept`](../../api/basic_json/accept.md) can be used unavailable, the parser can instead return a discarded value, or [`accept`](../../api/basic_json/accept.md) can be used
to only check whether an input is valid JSON. See [parsing and exceptions](parse_exceptions.md) for the available to only check whether an input is valid JSON. See [parsing and exceptions](parse_exceptions.md) for the available
options. To get as much as possible out of malformed input, a SAX parser can [recover from errors](error_recovery.md). options.
## See also ## See also
@@ -86,5 +86,4 @@ options. To get as much as possible out of malformed input, a SAX parser can [re
- [parser callbacks](parser_callbacks.md) - influence the parsing by a callback function - [parser callbacks](parser_callbacks.md) - influence the parsing by a callback function
- [SAX interface](sax_interface.md) - implement a custom SAX handler - [SAX interface](sax_interface.md) - implement a custom SAX handler
- [parsing and exceptions](parse_exceptions.md) - control error handling - [parsing and exceptions](parse_exceptions.md) - control error handling
- [error recovery](error_recovery.md) - get as much as possible out of malformed input
- [parsing untrusted input](untrusted_input.md) - what to consider when parsing input from untrusted sources - [parsing untrusted input](untrusted_input.md) - what to consider when parsing input from untrusted sources
@@ -64,8 +64,7 @@ bool parse_error(std::size_t position,
const json::exception& ex); const json::exception& ex);
``` ```
The return value decides whether to stop parsing (`#!cpp false`) or to repair the error and continue The return value indicates whether the parsing should continue, so the function should usually return `#!cpp false`.
(`#!cpp true`); see [error recovery](error_recovery.md) for the latter.
??? example "Example: report parse errors without exceptions" ??? example "Example: report parse errors without exceptions"
@@ -60,8 +60,7 @@ bool key(string_t& val);
bool parse_error(std::size_t position, const std::string& last_token, const json::exception& ex); bool parse_error(std::size_t position, const std::string& last_token, const json::exception& ex);
``` ```
The return value of each function determines whether parsing should proceed. For `parse_error`, returning The return value of each function determines whether parsing should proceed.
`#!cpp true` [recovers from the error](error_recovery.md).
To implement your own SAX handler, proceed as follows: To implement your own SAX handler, proceed as follows:
@@ -69,7 +68,7 @@ To implement your own SAX handler, proceed as follows:
2. Create an object of your SAX interface class, e.g. `my_sax`. 2. Create an object of your SAX interface class, e.g. `my_sax`.
3. Call `#!cpp bool json::sax_parse(input, &my_sax);` where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface. 3. Call `#!cpp bool json::sax_parse(input, &my_sax);` where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface.
Note the `sax_parse` function only returns a `#!cpp bool` indicating whether the input was parsed without errors and no SAX event returned `#!cpp false`. It does not return `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error - it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file `json_sax.hpp`. Note the `sax_parse` function only returns a `#!cpp bool` indicating the result of the last executed SAX event. It does not return `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error - it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file `json_sax.hpp`.
## See also ## See also
-1
View File
@@ -88,7 +88,6 @@ nav:
- features/performance.md - features/performance.md
- Parsing: - Parsing:
- features/parsing/index.md - features/parsing/index.md
- features/parsing/error_recovery.md
- features/parsing/json_lines.md - features/parsing/json_lines.md
- features/parsing/parse_exceptions.md - features/parsing/parse_exceptions.md
- features/parsing/parser_callbacks.md - features/parsing/parser_callbacks.md
File diff suppressed because it is too large. Load diff
+4 -9
View File
@@ -132,9 +132,7 @@ struct json_sax
@param[in] position the position in the input where the error occurs @param[in] position the position in the input where the error occurs
@param[in] last_token the last read token @param[in] last_token the last read token
@param[in] ex an exception object describing the error @param[in] ex an exception object describing the error
@return whether to recover from the error: false stops parsing; true @return whether parsing should proceed (must return false)
repairs the error and continues, or, if that is not possible,
stops after completing the value read so far
*/ */
virtual bool parse_error(std::size_t position, virtual bool parse_error(std::size_t position,
const std::string& last_token, const std::string& last_token,
@@ -328,12 +326,9 @@ a pointer to the respective array or object for each recursion depth.
After successful parsing, the value that is passed by reference to the After successful parsing, the value that is passed by reference to the
constructor contains the parsed value. constructor contains the parsed value.
@tparam BasicJsonType the JSON type @tparam BasicJsonType the JSON type
@tparam InputAdapterType the input adapter of the lexer that can be passed to
the constructor to record diagnostic positions; it
does not matter if no lexer is passed
*/ */
template<typename BasicJsonType, typename InputAdapterType = string_input_adapter_type> template<typename BasicJsonType, typename InputAdapterType>
class json_sax_dom_parser class json_sax_dom_parser
{ {
public: public:
@@ -557,7 +552,7 @@ class json_sax_dom_parser
lexer_t* m_lexer_ref = nullptr; lexer_t* m_lexer_ref = nullptr;
}; };
template<typename BasicJsonType, typename InputAdapterType = string_input_adapter_type> template<typename BasicJsonType, typename InputAdapterType>
class json_sax_dom_callback_parser class json_sax_dom_callback_parser
{ {
public: public:
+2 -591
View File
@@ -10,7 +10,7 @@
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t #include <cstdint> // uint32_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <initializer_list> // initializer_list #include <initializer_list> // initializer_list
#include <string> // char_traits, string #include <string> // char_traits, string
@@ -490,16 +490,8 @@ class lexer : public lexer_base<BasicJsonType>
if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF) if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF)
{ {
// expect next \uxxxx entry // expect next \uxxxx entry
if (JSON_HEDLEY_LIKELY(get() == '\\')) if (JSON_HEDLEY_LIKELY(get() == '\\' && get() == 'u'))
{ {
if (JSON_HEDLEY_UNLIKELY(get() != 'u'))
{
// current is the character escaped by the backslash
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
string_error_resume = resume_kind::escaped_character;
return token_type::parse_error;
}
const int codepoint2 = get_codepoint(); const int codepoint2 = get_codepoint();
if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1)) if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1))
@@ -524,11 +516,7 @@ class lexer : public lexer_base<BasicJsonType>
} }
else else
{ {
// the second escape was read completely and is a
// code point of its own
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF"; error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
string_error_resume = resume_kind::after_escape;
string_error_codepoint = codepoint2;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -542,9 +530,7 @@ class lexer : public lexer_base<BasicJsonType>
{ {
if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF)) if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF))
{ {
// the escape was read completely
error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF"; error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF";
string_error_resume = resume_kind::after_escape;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -2311,574 +2297,6 @@ scan_number_done:
} }
} }
public:
/////////////////////
// error recovery
/////////////////////
/*!
@brief make the best of the token that scan() rejected
Called by the parser after scan() returned token_type::parse_error and the
SAX parser asked to recover from the error (see #3989). Keeps what can be
read of the token and skips the rest:
- A string keeps its characters. An unknown escape stands for the escaped
character itself (as in JavaScript), an invalid Unicode escape and ill-formed
UTF-8 become U+FFFD, and a control character is kept. A line break or the
end of the input ends a string that lacks its closing quote.
- A number keeps its longest valid prefix, e.g. `1` for `1.` or `1e+`.
- A block comment that is not closed runs to the end of the input.
- Anything else is skipped.
The rest of an invalid token is skipped up to the next delimiter
(whitespace, a structural character, or a quote). A delimiter that the
invalid token consumed is returned to the input, so that the next scan()
reads it.
@return token_type::value_string or a number token type if a string or a
number could be read, token_type::end_of_input for a block comment
that is not closed, token_type::uninitialized otherwise
*/
token_type recover_token()
{
const resume_kind resume = string_error_resume;
const int codepoint = string_error_codepoint;
string_error_resume = resume_kind::character;
string_error_codepoint = -1;
if (error_message_starts_with("invalid string"))
{
return recover_string(resume, codepoint);
}
if (error_message_starts_with("invalid number"))
{
return recover_number();
}
if (error_message_starts_with("invalid comment; missing"))
{
// the comment runs to the end of the input
return token_type::end_of_input;
}
skip_to_delimiter();
return token_type::uninitialized;
}
/*!
@brief return the token that scan() read last to the input, so that the
next scan() reads it again
Called by the parser when recovering from an error. The token must be a
single character (',', ':', '[', ']', '{', or '}') or the end of the
input, and scan() must have read it last.
*/
void unget_token()
{
JSON_ASSERT(!next_unget);
unget();
}
/*!
@brief let the token string for the next error begin at the current character
The token string of an error reaches back to the beginning of the last
string or number. After an error, the parser calls this function so that
the next error does not report (and, with many errors, copy) everything
read since then.
*/
void restart_token_string()
{
restart_token_string_impl(std::integral_constant<bool, lazy_token_string> {});
}
private:
/// how recover_string() continues after the error scan_string() reported
enum class resume_kind : std::uint8_t
{
/// current is the next character of the string (or the end of input)
character,
/// current is the character escaped by the preceding backslash
escaped_character,
/// current is the last character of a complete escape
after_escape
};
/// whether error_message begins with @a prefix
bool error_message_starts_with(const char* prefix) const noexcept
{
const char* message = error_message;
while (*prefix != '\0')
{
if (*message++ != *prefix++)
{
return false;
}
}
return true;
}
/// whether current ends an invalid token (see recover_token())
bool current_is_delimiter() const noexcept
{
switch (current)
{
case ' ':
case '\t':
case '\n':
case '\r':
case '[':
case ']':
case '{':
case '}':
case ',':
case ':':
case '\"':
#if !JSON_STRICT_NUL_HANDLING
case '\0':
#endif
case char_traits<char_type>::eof():
return true;
case '/':
return ignore_comments;
default:
return false;
}
}
/// skip the rest of an invalid token and return its delimiter to the input
void skip_to_delimiter()
{
while (!current_is_delimiter())
{
get();
}
if (current != char_traits<char_type>::eof())
{
unget();
}
}
/// append U+FFFD REPLACEMENT CHARACTER to token_buffer
void add_replacement_character()
{
add(0xEF);
add(0xBF);
add(0xBD);
}
/// append the UTF-8 encoding of @a codepoint (not a surrogate) to token_buffer
void add_codepoint(const int codepoint)
{
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
const auto cp = static_cast<unsigned int>(codepoint);
if (cp < 0x80)
{
add(static_cast<char_int_type>(cp));
}
else if (cp <= 0x7FF)
{
add(static_cast<char_int_type>(0xC0u | (cp >> 6u)));
add(static_cast<char_int_type>(0x80u | (cp & 0x3Fu)));
}
else if (cp <= 0xFFFF)
{
add(static_cast<char_int_type>(0xE0u | (cp >> 12u)));
add(static_cast<char_int_type>(0x80u | ((cp >> 6u) & 0x3Fu)));
add(static_cast<char_int_type>(0x80u | (cp & 0x3Fu)));
}
else
{
add(static_cast<char_int_type>(0xF0u | (cp >> 18u)));
add(static_cast<char_int_type>(0x80u | ((cp >> 12u) & 0x3Fu)));
add(static_cast<char_int_type>(0x80u | ((cp >> 6u) & 0x3Fu)));
add(static_cast<char_int_type>(0x80u | (cp & 0x3Fu)));
}
}
/// append a code point read from a Unicode escape; a surrogate becomes U+FFFD
void add_escaped_codepoint(const int codepoint)
{
if (0xD800 <= codepoint && codepoint <= 0xDFFF)
{
add_replacement_character();
}
else
{
add_codepoint(codepoint);
}
}
/*!
@brief remove an incomplete UTF-8 sequence from the end of token_buffer
next_byte_in_range() adds the bytes of a sequence as it checks them, so
when it rejects a byte, the beginning of the sequence is already in
token_buffer, which otherwise holds only complete sequences.
@return whether an incomplete sequence was removed
*/
bool remove_incomplete_utf8_sequence()
{
std::size_t lead = token_buffer.size();
std::size_t continuation_bytes = 0;
while (lead > 0 && continuation_bytes < 3
&& (static_cast<unsigned char>(token_buffer[lead - 1]) & 0xC0u) == 0x80u)
{
--lead;
++continuation_bytes;
}
if (lead == 0)
{
return false;
}
const auto lead_byte = static_cast<unsigned char>(token_buffer[lead - 1]);
std::size_t expected = 0;
if (lead_byte >= 0xF0)
{
expected = 3;
}
else if (lead_byte >= 0xE0)
{
expected = 2;
}
else if (lead_byte >= 0xC0)
{
expected = 1;
}
if (continuation_bytes >= expected)
{
return false;
}
token_buffer.resize(lead - 1);
return true;
}
/*!
@brief read the UTF-8 sequence that begins with current, which is not ASCII
@return whether the next character must be read; false if current still
needs to be handled, because it does not belong to the sequence
*/
bool recover_utf8_sequence()
{
// the number of continuation bytes and the range of the first one;
// see the ranges in scan_string()
std::size_t count = 0;
char_int_type low = 0x80;
char_int_type high = 0xBF;
if (current >= 0xC2 && current <= 0xDF)
{
count = 1;
}
else if (current >= 0xE0 && current <= 0xEF)
{
count = 2;
low = (current == 0xE0) ? 0xA0 : 0x80;
high = (current == 0xED) ? 0x9F : 0xBF;
}
else if (current >= 0xF0 && current <= 0xF4)
{
count = 3;
low = (current == 0xF0) ? 0x90 : 0x80;
high = (current == 0xF4) ? 0x8F : 0xBF;
}
else
{
// an ill-formed byte
add_replacement_character();
return true;
}
const std::size_t start = token_buffer.size();
add(current);
for (std::size_t i = 0; i < count; ++i)
{
get();
if (current < low || current > high)
{
token_buffer.resize(start);
add_replacement_character();
return false;
}
add(current);
low = 0x80;
high = 0xBF;
}
return true;
}
/*!
@brief read the low surrogate that must follow the high surrogate @a high
@return whether the next character must be read; false if current still
needs to be handled
*/
bool recover_low_surrogate(int high)
{
while (true)
{
if (get() != '\\')
{
add_replacement_character();
return false;
}
if (get() != 'u')
{
add_replacement_character();
// not 'u', so this does not come back here
return recover_escape();
}
const int low = get_codepoint();
if (low == -1)
{
add_replacement_character();
return false;
}
if (0xDC00 <= low && low <= 0xDFFF)
{
add_codepoint(static_cast<int>((static_cast<unsigned int>(high) << 10u)
+ static_cast<unsigned int>(low) - 0x35FDC00u));
return true;
}
// high has no low surrogate
add_replacement_character();
if (low < 0xD800 || low > 0xDBFF)
{
add_codepoint(low);
return true;
}
// another high surrogate
high = low;
}
}
/*!
@brief read the escape whose backslash was read; current is the escaped character
@return whether the next character must be read; false if current still
needs to be handled
*/
bool recover_escape()
{
switch (current)
{
case '\"':
add('\"');
return true;
case '\\':
add('\\');
return true;
case '/':
add('/');
return true;
case 'b':
add('\b');
return true;
case 'f':
add('\f');
return true;
case 'n':
add('\n');
return true;
case 'r':
add('\r');
return true;
case 't':
add('\t');
return true;
case 'u':
{
const int codepoint = get_codepoint();
if (codepoint == -1)
{
add_replacement_character();
return false;
}
if (0xD800 <= codepoint && codepoint <= 0xDBFF)
{
return recover_low_surrogate(codepoint);
}
add_escaped_codepoint(codepoint);
return true;
}
// an unknown escape stands for the escaped character
default:
return false;
}
}
/*!
@brief read the rest of a string after scan_string() rejected it
token_buffer holds what scan_string() read before the error. See
recover_token() for how errors are repaired.
@param[in] resume how to continue, see resume_kind
@param[in] codepoint for a high surrogate followed by an escape of another
code point: that code point; -1 otherwise
*/
token_type recover_string(const resume_kind resume, const int codepoint)
{
// whether the next character must be read before it can be handled
bool fetch = false;
if (error_message_starts_with("invalid string: surrogate")
|| error_message_starts_with("invalid string: '\\u'")
|| (error_message_starts_with("invalid string: ill-formed UTF-8")
&& remove_incomplete_utf8_sequence()))
{
add_replacement_character();
}
switch (resume)
{
case resume_kind::escaped_character:
fetch = recover_escape();
break;
case resume_kind::after_escape:
if (0xD800 <= codepoint && codepoint <= 0xDBFF)
{
fetch = recover_low_surrogate(codepoint);
}
else
{
if (codepoint != -1)
{
add_escaped_codepoint(codepoint);
}
fetch = true;
}
break;
case resume_kind::character:
default:
break;
}
while (true)
{
if (fetch)
{
get();
}
fetch = true;
switch (current)
{
case '\"':
// a line break or the end of the input ends a string that
// lacks its closing quote
case '\n':
case '\r':
case char_traits<char_type>::eof():
return token_type::value_string;
#if !JSON_STRICT_NUL_HANDLING
case '\0':
// the end of the input, see scan()
unget();
return token_type::value_string;
#endif
case '\\':
get();
fetch = recover_escape();
break;
default:
if (current < 0x80)
{
// including control characters
add(current);
}
else
{
fetch = recover_utf8_sequence();
}
break;
}
}
}
/*!
@brief keep the longest valid prefix of a number that scan_number() rejected
token_buffer holds the characters scan_number() accepted before the error,
so the prefix ends at its last digit.
*/
token_type recover_number()
{
// only size(), operator[], and resize() are used, which every string
// type the library supports provides
std::size_t length = token_buffer.size();
while (length != 0 && (token_buffer[length - 1] < '0' || token_buffer[length - 1] > '9'))
{
--length;
}
token_buffer.resize(length);
if (length == 0)
{
skip_to_delimiter();
return token_type::uninitialized;
}
if (decimal_point_position >= length)
{
decimal_point_position = std::string::npos;
}
std::size_t exponent = std::string::npos;
for (std::size_t i = 0; i < length; ++i)
{
if (token_buffer[i] == 'e' || token_buffer[i] == 'E')
{
exponent = i;
break;
}
}
const std::size_t mantissa_end = (exponent == std::string::npos) ? length : exponent;
token_type number_type = token_type::value_unsigned;
if (decimal_point_position != std::string::npos || exponent != std::string::npos)
{
number_type = token_type::value_float;
}
else if (token_buffer[0] == '-')
{
number_type = token_type::value_integer;
}
const token_type result = convert_number(number_type, mantissa_end);
skip_to_delimiter();
return result;
}
/// seekable adapter: the token string begins at current, which was consumed
void restart_token_string_impl(std::true_type /*lazy*/) noexcept
{
const std::size_t consumed = ia.get_consumed_count();
token_string_start = (consumed > 0 && current != char_traits<char_type>::eof()) ? consumed - 1 : consumed;
}
/// streaming adapter: the token string begins at current; a character
/// that was put back is copied again when it is read again
void restart_token_string_impl(std::false_type /*lazy*/)
{
token_string.clear();
if (!next_unget && current != char_traits<char_type>::eof())
{
token_string.push_back(char_traits<char_type>::to_char_type(current));
}
}
/// input adapter /// input adapter
InputAdapterType ia; InputAdapterType ia;
@@ -2918,13 +2336,6 @@ scan_number_done:
/// a description of occurred lexer errors /// a description of occurred lexer errors
const char* error_message = ""; const char* error_message = "";
/// how recover_token() continues a string that scan_string() rejected;
/// set only on the error paths that need more than error_message
resume_kind string_error_resume = resume_kind::character;
/// the code point of the second escape when a high surrogate is followed
/// by an escape that is not a low surrogate; -1 otherwise
int string_error_codepoint = -1;
// number values // number values
number_integer_t value_integer = 0; number_integer_t value_integer = 0;
number_unsigned_t value_unsigned = 0; number_unsigned_t value_unsigned = 0;
+27 -729
View File
@@ -139,59 +139,24 @@ class parser
bool accept(const bool strict = true) bool accept(const bool strict = true)
{ {
json_sax_acceptor<BasicJsonType> sax_acceptor; json_sax_acceptor<BasicJsonType> sax_acceptor;
return sax_parse_impl<false>(&sax_acceptor, strict); return sax_parse(&sax_acceptor, strict);
} }
/*!
@brief public SAX interface
If the SAX parser's parse_error() returns true, the parser recovers from
the error: it repairs the input and continues (see #3989).
@param[in] sax the SAX parser
@param[in] strict whether to expect the last token to be EOF
@return whether the input was parsed without errors and no SAX event
returned false
*/
template<typename SAX> template<typename SAX>
JSON_HEDLEY_NON_NULL(2) JSON_HEDLEY_NON_NULL(2)
bool sax_parse(SAX* sax, const bool strict = true) bool sax_parse(SAX* sax, const bool strict = true)
{
return sax_parse_impl<true>(sax, strict);
}
private:
/// what sax_parse_internal() does after an object key was expected
enum class next_step : std::uint8_t
{
/// stop parsing
stop,
/// parse a value that begins with last_token
parse_value,
/// evaluate the state of the innermost container, which reads
/// last_token again
evaluate_state
};
template<bool AllowRecovery, typename SAX>
JSON_HEDLEY_NON_NULL(2)
bool sax_parse_impl(SAX* sax, const bool strict)
{ {
(void)detail::is_sax_static_asserts<SAX, BasicJsonType> {}; (void)detail::is_sax_static_asserts<SAX, BasicJsonType> {};
const bool result = sax_parse_internal<AllowRecovery>(sax); const bool result = sax_parse_internal(sax);
if (result) if (result)
{ {
if (strict) if (strict)
{ {
// strict mode: next byte must be EOF; after recovering from an // strict mode: next byte must be EOF
// error, the end of the input may already have been read if (get_token() != token_type::end_of_input)
if (last_token != token_type::end_of_input && get_token() != token_type::end_of_input)
{ {
// the value is complete, so there is nothing to recover return syntax_error(*sax, exception_message(token_type::end_of_input, "value"));
static_cast<void>(report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::end_of_input, "value"), nullptr),
std::integral_constant<bool, AllowRecovery> {}));
return false;
} }
} }
else else
@@ -202,9 +167,10 @@ class parser
} }
} }
return result && !error_reported; return result;
} }
private:
/*! /*!
@brief run a DOM SAX parser to completion and position the lexer @brief run a DOM SAX parser to completion and position the lexer
@@ -222,7 +188,7 @@ class parser
template<typename DomSax> template<typename DomSax>
bool parse_dom(DomSax& sdp, const bool strict) bool parse_dom(DomSax& sdp, const bool strict)
{ {
sax_parse_internal<false>(&sdp); sax_parse_internal(&sdp);
if (strict) if (strict)
{ {
@@ -242,20 +208,10 @@ class parser
return !sdp.is_errored(); return !sdp.is_errored();
} }
/*! template<typename SAX>
@brief parse a JSON value and pass it to a SAX parser
@tparam AllowRecovery whether to recover from an error if the SAX parser's
parse_error() returns true; false for the SAX parsers
of parse() and accept(), which never do, so that no
code for recovering is generated for them
*/
template<bool AllowRecovery, typename SAX>
JSON_HEDLEY_NON_NULL(2) JSON_HEDLEY_NON_NULL(2)
bool sax_parse_internal(SAX* sax) bool sax_parse_internal(SAX* sax)
{ {
const std::integral_constant<bool, AllowRecovery> allow_recovery{};
// stack to remember the hierarchy of structured values we are parsing // stack to remember the hierarchy of structured values we are parsing
// true = array; false = object // true = array; false = object
std::vector<bool> states; std::vector<bool> states;
@@ -286,18 +242,10 @@ class parser
break; break;
} }
// remember we are now inside an object // parse key
states.push_back(false);
// parse key (the steps of parse_key(), which are
// repeated here and below for speed)
if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
{ {
if (!continue_after(key_error(sax, allow_recovery, false), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
{
return false;
}
continue;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
{ {
@@ -307,13 +255,12 @@ class parser
// parse separator (:) // parse separator (:)
if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator)))
{ {
if (!continue_after(key_error(sax, allow_recovery, true), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::name_separator, "object separator"));
{
return false;
}
continue;
} }
// remember we are now inside an object
states.push_back(false);
// parse values // parse values
get_token(); get_token();
continue; continue;
@@ -349,11 +296,9 @@ class parser
if (JSON_HEDLEY_UNLIKELY(!std::isfinite(res))) if (JSON_HEDLEY_UNLIKELY(!std::isfinite(res)))
{ {
if (!overflow_error(sax, res, allow_recovery)) return sax->parse_error(m_lexer.get_position(),
{ m_lexer.get_token_string(),
return false; out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr));
}
break;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string()))) if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string())))
@@ -421,63 +366,16 @@ class parser
case token_type::parse_error: case token_type::parse_error:
{ {
// using "uninitialized" to avoid an "expected" message // using "uninitialized" to avoid an "expected" message
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::uninitialized, "value"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::uninitialized, "value"));
{
return false;
}
// recover: keep what can be read of the token
recover_token(allow_recovery);
if (last_token != token_type::uninitialized)
{
// a string or a number
continue;
}
if (states.empty())
{
// look for the value after the garbage
if (!skip_to_value(allow_recovery))
{
return false;
}
continue;
}
// nothing could be read
if (JSON_HEDLEY_UNLIKELY(!sax->null()))
{
return false;
}
break;
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
// there is nothing to recover return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON");
static_cast<void>(report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(),
"attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr), allow_recovery));
return false;
} }
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::literal_or_value, "value"));
{
return false;
}
// recover: the input ends where a value is missing
if (states.empty())
{
// there is no value
return false;
}
if (!recover_missing_value(sax, states, allow_recovery))
{
return false;
}
// the state evaluation reads the token again
unget_token(allow_recovery);
skip_to_state_evaluation = true;
continue;
} }
case token_type::uninitialized: case token_type::uninitialized:
case token_type::end_array: case token_type::end_array:
@@ -487,35 +385,7 @@ class parser
case token_type::literal_or_value: case token_type::literal_or_value:
default: // the last token was unexpected default: // the last token was unexpected
{ {
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::literal_or_value, "value"));
{
return false;
}
// recover
if (states.empty())
{
// look for the value after the garbage
if (!skip_to_value(allow_recovery))
{
return false;
}
continue;
}
if (last_token == token_type::name_separator)
{
// a stray ':'; the value may follow
get_token();
continue;
}
if (!recover_missing_value(sax, states, allow_recovery))
{
return false;
}
// the state evaluation reads the token again
unget_token(allow_recovery);
skip_to_state_evaluation = true;
continue;
} }
} }
} }
@@ -566,30 +436,7 @@ class parser
continue; continue;
} }
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::end_array, "array"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::end_array, "array"));
{
return false;
}
// recover
if (last_token == token_type::end_of_input)
{
// the input ends inside the array
return close_containers(sax, states, allow_recovery);
}
if (last_token == token_type::end_object)
{
// a wrong closing bracket closes the innermost container
if (JSON_HEDLEY_UNLIKELY(!sax->end_array()))
{
return false;
}
states.pop_back();
skip_to_state_evaluation = true;
}
// otherwise, a missing ',' (or a stray ':', which value
// parsing drops): the next value begins here
continue;
} }
// states.back() is false -> object // states.back() is false -> object
@@ -606,12 +453,9 @@ class parser
// parse key // parse key
if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
{ {
if (!continue_after(key_error(sax, allow_recovery, false), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
{
return false;
}
continue;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
{ {
return false; return false;
@@ -620,11 +464,7 @@ class parser
// parse separator (:) // parse separator (:)
if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator)))
{ {
if (!continue_after(key_error(sax, allow_recovery, true), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::name_separator, "object separator"));
{
return false;
}
continue;
} }
// parse values // parse values
@@ -652,546 +492,10 @@ class parser
continue; continue;
} }
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::end_object, "object"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::end_object, "object"));
{
return false;
}
// recover
if (last_token == token_type::end_of_input)
{
// the input ends inside the object
return close_containers(sax, states, allow_recovery);
}
if (last_token == token_type::end_array)
{
// a wrong closing bracket closes the innermost container
if (JSON_HEDLEY_UNLIKELY(!sax->end_object()))
{
return false;
}
states.pop_back();
skip_to_state_evaluation = true;
continue;
}
if (!continue_after(recover_member(sax, allow_recovery), skip_to_state_evaluation))
{
return false;
}
} }
} }
/*!
@brief continue sax_parse_internal() after a recovery
@return whether to continue parsing
*/
bool continue_after(const next_step step, bool& skip_to_state_evaluation)
{
if (step == next_step::evaluate_state)
{
// the state evaluation reads the token again
m_lexer.unget_token();
skip_to_state_evaluation = true;
}
return step != next_step::stop;
}
/// the parser for parse() and accept() never recovers: stop parsing
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type continue_after(std::false_type /*step*/, bool& /*skip_to_state_evaluation*/) noexcept
{
return {};
}
/*!
@brief parse an object key and the name separator (:) after it
last_token is the token where the key is expected. sax_parse_internal()
repeats these steps rather than calling this function, which is used
when recovering from an error.
@return next_step::parse_value if the value follows, with last_token its
first token; next_step::evaluate_state if the object's state is
to be evaluated after recovering from an error; next_step::stop
to stop parsing
*/
template<typename SAX>
next_step parse_key(SAX* sax)
{
const std::true_type allow_recovery{};
if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
{
return key_error(sax, allow_recovery, false);
}
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
{
return next_step::stop;
}
// parse separator (:)
if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator))
{
return key_error(sax, allow_recovery, true);
}
// the value begins with the next token
get_token();
return next_step::parse_value;
}
/*!
@brief report a number that is too large for number_float_t, and recover
from the error by passing the value on; the SAX parser gets the
number's text as well
This is a separate function, as reading other numbers is measurably
slower if the error is handled where they are read.
@param[in] sax the SAX parser
@param[in] value the value that is not finite
@return whether to continue parsing
*/
template<typename SAX>
bool overflow_error(SAX* sax, const number_float_t value, std::true_type allow_recovery)
{
if (!report_error(sax, out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr), allow_recovery))
{
return false;
}
return sax->number_float(value, m_lexer.get_string());
}
/// @copydoc overflow_error
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type overflow_error(SAX* sax, const number_float_t /*value*/, std::false_type allow_recovery)
{
return report_error(sax, out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr), allow_recovery);
}
/*!
@brief report a missing key, or a missing name separator (:) after the
key; the parser for parse() and accept() never recovers
@param[in] key_read whether the key was read, so that the name separator
is missing
@return std::false_type, see report_error()
*/
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type key_error(SAX* sax, std::false_type allow_recovery, const bool key_read)
{
return report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), key_read
? exception_message(token_type::name_separator, "object separator")
: exception_message(token_type::value_string, "object key"), nullptr), allow_recovery);
}
/*!
@brief report a missing key, or a missing name separator (:) after the
key, and recover from it
@param[in] key_read whether the key was read, so that the name separator
is missing
*/
template<typename SAX>
next_step key_error(SAX* sax, std::true_type allow_recovery, const bool key_read)
{
if (!key_read)
{
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), nullptr), allow_recovery))
{
return next_step::stop;
}
return recover_key(sax);
}
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), nullptr), allow_recovery))
{
return next_step::stop;
}
return recover_name_separator(sax);
}
/////////////////////
// error recovery
/////////////////////
/*
The functions below repair an error after the SAX parser's parse_error()
returned true (see #3989). Each mistake is repaired by the smallest local
edit: a missing ',' or ':' is inserted, a stray token is removed, what can
be read of an invalid string or number is kept (see
lexer::recover_token()), a missing value becomes null, a wrong closing
bracket closes the innermost container, and the end of the input closes
all of them. The events stay balanced, and every key() is followed by
exactly one value.
A repair hands a token to the state evaluation, by returning it to the
lexer (lexer::unget_token()) so that the state evaluation reads it again,
only if it is ',', ']', '}', or the end of the input. The state evaluation
hands a token to value or key parsing only if it is none of them, so a
token is never handed back and forth. Every other step reads a token or
closes a container, so parsing always ends.
*/
/*!
@brief report an error to the SAX parser; the parser for parse() and
accept() never recovers
@return std::false_type rather than false: its value is known where the
function is called even if the call is not inlined, so the code
for recovering is not generated
*/
template<typename SAX, typename Exception>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type report_error(SAX* sax, const Exception& ex, std::false_type /*allow_recovery*/)
{
static_cast<void>(sax); // MSVC 2015 does not count calling a static parse_error() as using it
error_reported = true;
static_cast<void>(sax->parse_error(m_lexer.get_position(), m_lexer.get_token_string(), ex));
return {};
}
/*!
@brief report an error to the SAX parser
@return whether to recover from the error
*/
template<typename SAX, typename Exception>
bool report_error(SAX* sax, const Exception& ex, std::true_type /*allow_recovery*/)
{
static_cast<void>(sax); // MSVC 2015 does not count calling a static parse_error() as using it
const std::size_t position = m_lexer.get_position().chars_read_total;
if (error_reported && position == last_error_position && last_token == last_error_token)
{
// a repair handed on the token of the error it repaired; the
// token was reported already, and the SAX parser asked to recover
return true;
}
error_reported = true;
last_error_position = position;
last_error_token = last_token;
if (!sax->parse_error(m_lexer.get_position(), m_lexer.get_token_string(), ex))
{
return false;
}
// the token string of the next error begins here
m_lexer.restart_token_string();
return true;
}
/*!
@brief keep what can be read of the token that the lexer rejected
The error was reported for the rejected token, so it is not reported again
for the token it is repaired to (see lexer::recover_token()).
*/
token_type recover_token()
{
last_token = m_lexer.recover_token();
last_error_position = m_lexer.get_position().chars_read_total;
last_error_token = last_token;
return last_token;
}
// The functions below are called from sax_parse_internal() after an error
// was reported. The parser for parse() and accept() stopped there, so for
// it they are stubs: the code for recovering is then not even referenced,
// and unoptimized builds do not emit it either.
/// @copydoc recover_token()
JSON_INTERNAL_ALWAYS_INLINE
token_type recover_token(std::true_type /*allow_recovery*/)
{
return recover_token();
}
/// @copydoc recover_token()
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type recover_token(std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/// return the token that was read last to the lexer (see lexer::unget_token())
JSON_INTERNAL_ALWAYS_INLINE
void unget_token(std::true_type /*allow_recovery*/)
{
m_lexer.unget_token();
}
/// @copydoc unget_token(std::true_type)
JSON_INTERNAL_ALWAYS_INLINE
static void unget_token(std::false_type /*allow_recovery*/) noexcept {}
/// @copydoc skip_to_value
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type skip_to_value(std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/// @copydoc recover_missing_value
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type recover_missing_value(SAX* /*sax*/, const std::vector<bool>& /*states*/, std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/// pass the end events of all open containers
template<typename SAX>
bool close_containers(SAX* sax, std::vector<bool>& states, std::true_type /*allow_recovery*/)
{
while (!states.empty())
{
const bool is_array = states.back();
states.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_array ? !sax->end_array() : !sax->end_object()))
{
return false;
}
}
return true;
}
/// @copydoc close_containers
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type close_containers(SAX* /*sax*/, std::vector<bool>& /*states*/, std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/*!
@brief read tokens until one begins a value, skipping everything before
the top-level value
@return whether a value begins with last_token
*/
bool skip_to_value(std::true_type /*allow_recovery*/)
{
while (true)
{
switch (get_token())
{
case token_type::begin_array:
case token_type::begin_object:
case token_type::literal_false:
case token_type::literal_null:
case token_type::literal_true:
case token_type::value_float:
case token_type::value_integer:
case token_type::value_string:
case token_type::value_unsigned:
return true;
case token_type::end_of_input:
return false;
case token_type::parse_error:
recover_token();
if (last_token != token_type::uninitialized)
{
return true;
}
break;
case token_type::uninitialized:
case token_type::end_array:
case token_type::end_object:
case token_type::name_separator:
case token_type::value_separator:
case token_type::literal_or_value:
default:
break;
}
}
}
/*!
@brief skip the rest of an object member that cannot be read
Reads tokens, beginning with last_token, until a ',', '}', or ']' that is
not inside a container that begins in the skipped tokens, or the end of
the input.
*/
void skip_member()
{
std::size_t depth = 0;
while (true)
{
switch (last_token)
{
case token_type::begin_array:
case token_type::begin_object:
++depth;
break;
case token_type::end_array:
case token_type::end_object:
if (depth == 0)
{
return;
}
--depth;
break;
case token_type::value_separator:
if (depth == 0)
{
return;
}
break;
case token_type::end_of_input:
return;
case token_type::parse_error:
recover_token();
break;
case token_type::uninitialized:
case token_type::literal_true:
case token_type::literal_false:
case token_type::literal_null:
case token_type::value_string:
case token_type::value_unsigned:
case token_type::value_integer:
case token_type::value_float:
case token_type::name_separator:
case token_type::literal_or_value:
default:
break;
}
get_token();
}
}
/*!
@brief pass a value where it is missing
last_token is ',', ']', '}', or the end of the input, where a value was
expected. In an object, the key gets null; in an array, a ',' where a
value is missing stands for null (as in JavaScript), while an array that
ends there just ends.
*/
template<typename SAX>
bool recover_missing_value(SAX* sax, const std::vector<bool>& states, std::true_type /*allow_recovery*/)
{
JSON_ASSERT(!states.empty());
if (!states.back() || last_token == token_type::value_separator)
{
return sax->null();
}
return true;
}
/// recover from a missing key; last_token is where it was expected
template<typename SAX>
next_step recover_key(SAX* sax)
{
switch (last_token)
{
case token_type::value_separator:
case token_type::end_object:
case token_type::end_array:
case token_type::end_of_input:
// no member: the object's state handles the token
return next_step::evaluate_state;
case token_type::parse_error:
recover_token();
if (last_token == token_type::value_string)
{
// a key that could be repaired
return parse_key(sax);
}
skip_member();
return next_step::evaluate_state;
case token_type::uninitialized:
case token_type::literal_true:
case token_type::literal_false:
case token_type::literal_null:
case token_type::value_string:
case token_type::value_unsigned:
case token_type::value_integer:
case token_type::value_float:
case token_type::begin_array:
case token_type::begin_object:
case token_type::name_separator:
case token_type::literal_or_value:
default:
// a member without a key
skip_member();
return next_step::evaluate_state;
}
}
/// recover from a missing name separator (:) after the key; last_token
/// is where it was expected
template<typename SAX>
next_step recover_name_separator(SAX* sax)
{
switch (last_token)
{
case token_type::value_separator:
case token_type::end_object:
case token_type::end_array:
case token_type::end_of_input:
// the value is missing as well
return sax->null() ? next_step::evaluate_state : next_step::stop;
case token_type::uninitialized:
case token_type::literal_true:
case token_type::literal_false:
case token_type::literal_null:
case token_type::value_string:
case token_type::value_unsigned:
case token_type::value_integer:
case token_type::value_float:
case token_type::begin_array:
case token_type::begin_object:
case token_type::name_separator:
case token_type::parse_error:
case token_type::literal_or_value:
default:
// a missing ':'; the value begins here
return next_step::parse_value;
}
}
/// recover from a token after an object member that is neither ',' nor
/// '}' (nor ']' or the end of the input, which the caller handles)
template<typename SAX>
next_step recover_member(SAX* sax, std::true_type /*allow_recovery*/)
{
if (last_token == token_type::parse_error)
{
recover_token();
}
if (last_token == token_type::value_string)
{
// a missing ','; the next key begins here
return parse_key(sax);
}
skip_member();
return next_step::evaluate_state;
}
/// the parser for parse() and accept() never recovers (and does not come
/// here, as report_error() returned false)
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type recover_member(SAX* /*sax*/, std::false_type /*allow_recovery*/) const noexcept
{
return {};
}
/// get next token from lexer /// get next token from lexer
token_type get_token() token_type get_token()
{ {
@@ -1262,12 +566,6 @@ class parser
const bool allow_exceptions = true; const bool allow_exceptions = true;
/// whether trailing commas in objects and arrays should be ignored (true) or signaled as errors (false) /// whether trailing commas in objects and arrays should be ignored (true) or signaled as errors (false)
const bool ignore_trailing_commas = false; const bool ignore_trailing_commas = false;
/// whether an error was reported to the SAX parser
bool error_reported = false;
/// the position of the last reported error
std::size_t last_error_position = 0;
/// the token of the last reported error
token_type last_error_token = token_type::uninitialized;
}; };
} // namespace detail } // namespace detail
+376 -183
View File
@@ -9,10 +9,13 @@
#pragma once #pragma once
#include <algorithm> // all_of #include <algorithm> // all_of
#include <array> // array
#include <cctype> // isdigit #include <cctype> // isdigit
#include <cerrno> // errno, ERANGE #include <cerrno> // errno, ERANGE
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint64_t
#include <cstdlib> // strtoull #include <cstdlib> // strtoull
#include <cstring> // memcmp, memcpy
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
#include <iosfwd> // ostream #include <iosfwd> // ostream
#endif // JSON_NO_IO #endif // JSON_NO_IO
@@ -24,6 +27,7 @@
#include <nlohmann/detail/exceptions.hpp> #include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp> #include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/string_concat.hpp> #include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp> #include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp> #include <nlohmann/detail/string_utils.hpp>
@@ -492,50 +496,111 @@ class json_pointer
private: private:
/*! /*!
@brief the pointer prefixes of a flattened object, and which of them denote arrays @brief the pointer prefixes of a flattened object that denote arrays
@ref unflatten collects the pointer prefixes that have a reference token 0 @ref unflatten creates arrays exactly below the prefixes that have a
among their children; @ref get_and_create creates arrays exactly below reference token 0 among their children, and objects everywhere else.
those prefixes and objects everywhere else. Deciding this up front keeps Deciding this up front keeps the result independent of the order in which
the result independent of the order in which the flattened object is the flattened object is iterated, which is unspecified for some object
iterated, which is unspecified for some object types. types.
The prefixes form a tree and are numbered (number 0 is the empty prefix), A prefix is identified by the text of the key it is the beginning of:
so each of them is stored only once (as a node) rather than as a copy of escaping is unambiguous, so equal prefixes of valid keys are equal texts.
all of its reference tokens. The tree does not own the reference tokens: The set refers to the keys, which must outlive it.
it refers to the tokens of the parsed keys, which must outlive it.
*/ */
struct prefix_tree class array_prefix_set
{ {
// open-addressing hash table from (parent, last reference token) to public:
// the number of the node; empty slots have token == nullptr void insert(const char* data, std::size_t size)
{
// flattened objects are usually sorted, so most keys that have a
// 0 yield the prefix of the previous one
if (last_data != nullptr && last_size == size && std::memcmp(last_data, data, size) == 0)
{
return;
}
last_data = data;
last_size = size;
if (slots.empty())
{
slots.resize(16);
}
const std::size_t hash = hash_of(data, size);
const std::size_t mask = slots.size() - 1;
std::size_t i = hash & mask;
while (slots[i].data != nullptr)
{
if (slots[i].hash == hash && slots[i].size == size && std::memcmp(slots[i].data, data, size) == 0)
{
return;
}
i = (i + 1) & mask;
}
slots[i] = slot(hash, data, size);
// keep the load factor below 1/2
if (++used * 2 > slots.size())
{
grow();
}
}
bool empty() const noexcept
{
return used == 0;
}
bool contains(const char* data, std::size_t size) const noexcept
{
if (used == 0)
{
return false;
}
const std::size_t hash = hash_of(data, size);
const std::size_t mask = slots.size() - 1;
for (std::size_t i = hash & mask; slots[i].data != nullptr; i = (i + 1) & mask)
{
if (slots[i].hash == hash && slots[i].size == size && std::memcmp(slots[i].data, data, size) == 0)
{
return true;
}
}
return false;
}
private:
struct slot struct slot
{ {
std::size_t hash; slot() noexcept = default;
std::size_t parent;
const string_t* token;
std::size_t id;
slot() noexcept slot(std::size_t h, const char* d, std::size_t s) noexcept
: hash(0), parent(0), token(nullptr), id(0) : hash(h), data(d), size(s)
{} {}
slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept std::size_t hash = 0;
: hash(h), parent(p), token(t), id(i) const char* data = nullptr; // nullptr: the slot is empty
{} std::size_t size = 0;
}; };
std::vector<slot> slots; static std::size_t hash_of(const char* data, std::size_t size) noexcept
std::size_t used = 0;
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
{ {
std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL & (std::numeric_limits<std::size_t>::max)()); std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size;
for (std::size_t i = 0; i < token.size(); ++i) std::size_t i = 0;
for (; i + 8 <= size; i += 8)
{ {
h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL & (std::numeric_limits<std::size_t>::max)()); std::uint64_t w = 0;
std::memcpy(&w, data + i, 8);
h = (h ^ w) * 0x100000001B3ULL;
h ^= h >> 32u;
} }
return h ^ (h >> 29u); std::uint64_t w = 0;
if (i < size)
{
std::memcpy(&w, data + i, size - i);
}
h = (h ^ w) * 0x9E3779B97F4A7C15ULL;
return detail::conditional_static_cast<std::size_t>(h ^ (h >> 29u));
} }
void grow() void grow()
@@ -545,10 +610,10 @@ class json_pointer
const std::size_t mask = slots.size() - 1; const std::size_t mask = slots.size() - 1;
for (const auto& s : old) for (const auto& s : old)
{ {
if (s.token != nullptr) if (s.data != nullptr)
{ {
std::size_t i = s.hash & mask; std::size_t i = s.hash & mask;
while (slots[i].token != nullptr) while (slots[i].data != nullptr)
{ {
i = (i + 1) & mask; i = (i + 1) & mask;
} }
@@ -557,117 +622,160 @@ class json_pointer
} }
} }
// parent[id] is the number of the prefix without its last token std::vector<slot> slots{};
std::vector<std::size_t> parent; std::size_t used = 0;
// is_array[id] is true iff some flattened key has the reference token const char* last_data = nullptr;
// 0 directly below the prefix with number id std::size_t last_size = 0;
std::vector<bool> is_array;
// start with the empty prefix only
prefix_tree()
: slots(16)
, parent(1, 0)
, is_array(1, false)
{}
// return the number of the prefix with number id extended by
// reference_token, adding it if it is new
std::size_t add_child(std::size_t id, const string_t& reference_token)
{
const std::size_t hash = hash_of(id, reference_token);
const std::size_t mask = slots.size() - 1;
std::size_t i = hash & mask;
while (slots[i].token != nullptr)
{
if (slots[i].hash == hash && slots[i].parent == id && *slots[i].token == reference_token)
{
return slots[i].id;
}
i = (i + 1) & mask;
}
// new prefix
const std::size_t child = parent.size();
slots[i] = slot(hash, id, &reference_token, child);
if (reference_token == "0")
{
is_array[id] = true;
}
parent.push_back(id);
is_array.push_back(false);
// keep the load factor below 1/2
if (++used * 2 > slots.size())
{
grow();
}
return child;
}
}; };
/// @brief the length of the longest common prefix of the first @a limit bytes of @a a and @a b
static std::size_t common_prefix_length(const char* a, const char* b, const std::size_t limit) noexcept
{
std::size_t i = 0;
for (; i + 8 <= limit; i += 8)
{
std::uint64_t x = 0;
std::uint64_t y = 0;
std::memcpy(&x, a + i, 8);
std::memcpy(&y, b + i, 8);
if (x != y)
{
break;
}
}
while (i < limit && a[i] == b[i])
{
++i;
}
return i;
}
/*! /*!
@brief create and return a reference to the pointed to value @brief check that @a key is a valid JSON pointer, and add the prefixes of
it that denote arrays to @a arrays
@param[in] j the value to create the pointed to value in @throw parse_error.107 if the key is not empty or begins with '/'
@param[in] tokens the reference tokens of the pointer @throw parse_error.108 if character '~' is not followed by '0' or '1'
@param[in] prefixes prefixes[i] is the number in @a tree of the prefix
consisting of the first i reference tokens
Complexity: Linear in the number of reference tokens. The same checks as @ref split, which throws the same exceptions, but
without building the reference tokens.
*/
static void scan_key(const string_t& key, const string_t* previous, array_prefix_set& arrays)
{
const std::size_t size = key.size();
if (size == 0)
{
return;
}
@throw parse_error.106 if an array index begins with '0' const char* const data = key.data();
@throw parse_error.109 if array index is not a number if (JSON_HEDLEY_UNLIKELY(data[0] != '/'))
@throw type_error.313 if value cannot be unflattened {
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", key, "'"), nullptr));
}
// The beginning of the key that it has in common with the previous
// one, which was checked already, is skipped: the scan starts with
// the token after the last slash in it.
std::size_t begin = 1;
if (previous != nullptr)
{
std::size_t same = common_prefix_length(data, previous->data(), (std::min)(size, previous->size()));
while (same > 1 && data[same - 1] != '/')
{
--same;
}
if (same > 1)
{
begin = same;
}
}
for (std::size_t i = begin; i <= size; ++i)
{
if (i == size || data[i] == '/')
{
// a token 0 makes the pointer before it an array
if (i - begin == 1 && data[begin] == '0')
{
arrays.insert(data, begin - 1);
}
begin = i + 1;
}
else if (data[i] == '~')
{
// ~ must be followed by 0 or 1
if (JSON_HEDLEY_UNLIKELY(i + 1 == size || (data[i + 1] != '0' && data[i + 1] != '1')))
{
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
}
}
}
}
/*!
@brief resolve a plain JSON pointer without building its reference tokens
@param[in] ptr the value the pointer starts at
@param[in] path a JSON pointer string
@param[in] length the number of leading bytes of @a path to resolve; 0 or
the position of a slash
@param[in,out] token a string to hold the reference tokens, to reuse its storage
@return the pointed to value, or `nullptr` if the pointer does not denote
an existing value, or does so in a way that is not plain: a
reference token with an escape, an array index that is malformed or
out of range, or a "-". No exception is thrown for these; callers
use @ref json_pointer instead, which handles them.
*/ */
template<typename BasicJsonType> template<typename BasicJsonType>
static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens, static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token)
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
{ {
auto* result = &j; for (std::size_t pos = 0; pos < length;)
// in case no reference tokens exist, return a reference to the JSON value
// j which will be overwritten by a primitive value
for (std::size_t i = 0; i < tokens.size(); ++i)
{ {
const auto& reference_token = tokens[i]; if (JSON_HEDLEY_UNLIKELY(path[pos] != '/'))
switch (result->type())
{ {
case detail::value_t::null: return nullptr;
{ }
if (tree.is_array[prefixes[i]])
{
// some reference token below this position is 0, so the
// value is an array
result = &result->operator[](array_index<BasicJsonType>(reference_token));
}
else
{
// start a new object otherwise
result = &result->operator[](reference_token);
}
break;
}
// the reference token between the slash at pos and the next one
std::size_t end = path.find_first_of('/', pos + 1);
if (end == string_t::npos || end > length)
{
end = length;
}
token.clear();
token.append(path.data() + pos + 1, end - pos - 1);
if (JSON_HEDLEY_UNLIKELY(token.find_first_of('~') != string_t::npos))
{
return nullptr;
}
switch (ptr->type())
{
case detail::value_t::object: case detail::value_t::object:
{ {
// create an entry in the object const auto it = ptr->m_data.m_value.object->find(token);
result = &object_member(*result, reference_token); if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
{
return nullptr;
}
ptr = &it->second;
break; break;
} }
case detail::value_t::array: case detail::value_t::array:
{ {
// create an entry in the array typename BasicJsonType::size_type idx{};
result = &result->operator[](array_index<BasicJsonType>(reference_token)); if (JSON_HEDLEY_UNLIKELY(parse_array_index<BasicJsonType>(token, idx) != array_index_status::ok || idx >= ptr->m_data.m_value.array->size()))
{
return nullptr;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
break; break;
} }
/* case detail::value_t::null:
The following code is only reached if there exists a reference
token _and_ the current value is primitive. In this case, we have
an error situation, because primitive values may only occur as
a single value; that is, with an empty list of reference tokens.
*/
case detail::value_t::string: case detail::value_t::string:
case detail::value_t::boolean: case detail::value_t::boolean:
case detail::value_t::number_integer: case detail::value_t::number_integer:
@@ -676,11 +784,12 @@ class json_pointer
case detail::value_t::binary: case detail::value_t::binary:
case detail::value_t::discarded: case detail::value_t::discarded:
default: default:
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j)); return nullptr;
} }
pos = end;
} }
return *result; return ptr;
} }
/*! /*!
@@ -1135,20 +1244,45 @@ class json_pointer
// Add the entry (key, v) to the object `obj`. Flattened paths are pairwise // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise
// distinct, so objects that can append a key known to be absent // distinct, so objects that can append a key known to be absent
// (ordered_map) skip the lookup, which is linear and would make flatten // (ordered_map) skip the lookup, which is linear and would make flatten
// quadratic; all other object types use operator[]. // quadratic. Objects with a positional insert (std::map) insert next to
// the entry added last: paths come out in nearly sorted order, so the
// position is usually right and no search from the root is needed.
// All other object types use operator[].
template<typename ObjectType, typename ValueType> template<typename ObjectType, typename ValueType>
static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/) static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/)
-> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void()) -> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void())
{ {
obj.emplace_new_key(key, std::forward<ValueType>(v)); obj.emplace_new_key(key, std::forward<ValueType>(v));
} }
template<typename ObjectType, typename ValueType> template<typename ObjectType, typename ValueType>
static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/) static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
-> decltype(hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v)), void())
{
hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v));
++hint;
}
template<typename ObjectType, typename ValueType>
static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
{ {
obj[key] = std::forward<ValueType>(v); obj[key] = std::forward<ValueType>(v);
} }
/// append the decimal representation of @a i to @a out
static void append_index(string_t& out, std::size_t i)
{
std::array<char, 24> buf{};
std::size_t pos = buf.size();
do
{
buf[--pos] = static_cast<char>('0' + (i % 10));
i /= 10;
}
while (i != 0);
out.append(buf.data() + pos, buf.size() - pos);
}
/*! /*!
@param[in] reference_string the reference string to the current value @param[in] reference_string the reference string to the current value
@param[in] value the value to consider @param[in] value the value to consider
@@ -1194,7 +1328,8 @@ class json_pointer
// flatten `v`, whose path is `path`: primitives and empty containers // flatten `v`, whose path is `path`: primitives and empty containers
// are added to the result right away; other containers get a frame // are added to the result right away; other containers get a frame
auto& object = *result.m_data.m_value.object; auto& object = *result.m_data.m_value.object;
const auto enter = [&stack, &path, &object](const BasicJsonType & v) auto hint = object.end();
const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v)
{ {
switch (v.type()) switch (v.type())
{ {
@@ -1203,7 +1338,7 @@ class json_pointer
if (v.m_data.m_value.array->empty()) if (v.m_data.m_value.array->empty())
{ {
// flatten empty array as null // flatten empty array as null
add_flattened(object, path, nullptr, detail::priority_tag<1> {}); add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
} }
else else
{ {
@@ -1217,7 +1352,7 @@ class json_pointer
if (v.m_data.m_value.object->empty()) if (v.m_data.m_value.object->empty())
{ {
// flatten empty object as null // flatten empty object as null
add_flattened(object, path, nullptr, detail::priority_tag<1> {}); add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
} }
else else
{ {
@@ -1237,7 +1372,7 @@ class json_pointer
default: default:
{ {
// add a primitive value with its reference string // add a primitive value with its reference string
add_flattened(object, path, v, detail::priority_tag<1> {}); add_flattened(object, hint, path, v, detail::priority_tag<2> {});
return; return;
} }
} }
@@ -1265,7 +1400,8 @@ class json_pointer
// iterate array and use index as a reference string // iterate array and use index as a reference string
++stack.back().index; ++stack.back().index;
detail::concat_into(path, '/', detail::to_string<string_t>(i)); path.push_back('/');
append_index(path, i);
enter(array[i]); enter(array[i]);
} }
else else
@@ -1279,7 +1415,8 @@ class json_pointer
// iterate object and use keys as reference string // iterate object and use keys as reference string
++stack.back().member; ++stack.back().member;
detail::concat_into(path, '/', detail::escape(it->first)); path.push_back('/');
detail::escape_into(path, it->first);
enter(it->second); enter(it->second);
} }
} }
@@ -1310,53 +1447,30 @@ class json_pointer
BasicJsonType result; BasicJsonType result;
// parse every key once; the reference tokens are kept for the second // check every key, and collect the prefixes that have a reference
// pass (the vectors are never resized, so the tree may refer to them) // token 0 among their children; the values below them are arrays,
std::vector<std::vector<string_t>> keys; // all others are objects (see array_prefix_set)
keys.reserve(value.m_data.m_value.object->size()); array_prefix_set arrays;
const string_t* previous_key = nullptr;
for (const auto& element : *value.m_data.m_value.object) for (const auto& element : *value.m_data.m_value.object)
{ {
json_pointer ptr(element.first); scan_key(element.first, previous_key, arrays);
keys.push_back(std::move(ptr.reference_tokens)); previous_key = &element.first;
} }
// collect the pointer prefixes that have a reference token 0 among // values[i] is the value after the first i reference tokens of the
// their children; the values below them are arrays, all others are // previous key, ends[i] the length of that prefix of the key. Keys are
// objects (see prefix_tree) // often sorted, so most of the path to the value of a key is the
prefix_tree tree; // same as for the previous key and need not be looked up again. Only
// the number of the prefix with i + 1 tokens of the previous key; keys // the values below the common prefix are dropped, and only they can
// are often sorted, so most of the prefix is the same as for the // move when a value is added: the ones above are not inside the
// previous key and need not be looked up again // container that grows.
std::vector<std::size_t> path; std::vector<BasicJsonType*> values(1, &result);
const std::vector<string_t>* previous = nullptr; std::vector<std::size_t> ends(1, 0);
// the number of the complete prefix (i.e., the key) of each key const string_t* previous = nullptr;
std::vector<std::size_t> leaves; string_t token;
leaves.reserve(keys.size());
for (const auto& tokens : keys)
{
std::size_t common = 0;
if (previous != nullptr)
{
const std::size_t limit = (std::min)(tokens.size(), previous->size());
while (common < limit && tokens[common] == (*previous)[common])
{
++common;
}
}
path.resize(common);
std::size_t id = common == 0 ? 0 : path[common - 1];
for (std::size_t i = common; i < tokens.size(); ++i)
{
id = tree.add_child(id, tokens[i]);
path.push_back(id);
}
leaves.push_back(id);
previous = &tokens;
}
// iterate the JSON object values // iterate the JSON object values
std::vector<std::size_t> prefixes;
std::size_t key_index = 0;
for (const auto& element : *value.m_data.m_value.object) for (const auto& element : *value.m_data.m_value.object)
{ {
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
@@ -1364,21 +1478,100 @@ class json_pointer
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second)); JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second));
} }
const auto& tokens = keys[key_index]; const string_t& key = element.first;
// number the prefixes of this key from its last one to the first const char* const data = key.data();
prefixes.resize(tokens.size());
for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i) // the number of reference tokens the key has in common with the
// previous one
std::size_t depth = 0;
if (previous != nullptr)
{ {
id = tree.parent[id]; const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size()));
prefixes[i - 1] = id; depth = ends.size() - 1;
while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/')))
{
--depth;
}
} }
++key_index; values.resize(depth + 1);
ends.resize(depth + 1);
previous = &key;
// Assign the value to the reference pointed to by JSON pointer. Note // Assign the value to the reference pointed to by JSON pointer. Note
// that if the JSON pointer is "" (i.e., points to the whole value), // that if the JSON pointer is "" (i.e., points to the whole value),
// function get_and_create returns a reference to the result itself. // the value is the result itself. An assignment will then create a
// An assignment will then create a primitive value. // primitive value.
get_and_create(result, tokens, prefixes, tree) = element.second; BasicJsonType* current = values.back();
for (std::size_t pos = ends.back(); pos < key.size();)
{
// the reference token between the slash at pos and the next
std::size_t end = key.find_first_of('/', pos + 1);
if (end == string_t::npos)
{
end = key.size();
}
token.clear();
token.append(data + pos + 1, end - pos - 1);
if (token.find_first_of('~') != string_t::npos)
{
detail::unescape(token);
}
switch (current->type())
{
case detail::value_t::null:
{
if (arrays.contains(data, pos))
{
// some reference token below this position is 0, so the
// value is an array
current = &current->operator[](array_index<BasicJsonType>(token));
}
else
{
// start a new object otherwise
current = &current->operator[](token);
}
break;
}
case detail::value_t::object:
{
// create an entry in the object
current = &object_member(*current, token);
break;
}
case detail::value_t::array:
{
// create an entry in the array
current = &current->operator[](array_index<BasicJsonType>(token));
break;
}
/*
The following code is only reached if there exists a reference
token _and_ the current value is primitive. In this case, we have
an error situation, because primitive values may only occur as
a single value; that is, with an empty list of reference tokens.
*/
case detail::value_t::string:
case detail::value_t::boolean:
case detail::value_t::number_integer:
case detail::value_t::number_unsigned:
case detail::value_t::number_float:
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result));
}
values.push_back(current);
ends.push_back(end);
pos = end;
}
*current = element.second;
} }
return result; return result;
-11
View File
@@ -199,17 +199,6 @@
#define JSON_NO_UNIQUE_ADDRESS #define JSON_NO_UNIQUE_ADDRESS
#endif #endif
// Inlines small functions even in unoptimized builds, so that they are not
// emitted. The parsers for parse(), accept(), and from_*() use it for the
// functions that stand in for the code recovering from errors (see #3989).
// MSVC is left to decide, as it warns (C4714) where it does not inline a
// __forceinline function.
#if defined(_MSC_VER) && !defined(__clang__)
#define JSON_INTERNAL_ALWAYS_INLINE
#else
#define JSON_INTERNAL_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
#endif
// Clang targeting MinGW does not survive the thread_local storage the copy // Clang targeting MinGW does not survive the thread_local storage the copy
// constructor uses to bound its descent: every test that copies a value // constructor uses to bound its descent: every test that copies a value
// segfaults with clang 11.0.1 and clang 18.1.8, while the same tests pass with // segfaults with clang 11.0.1 and clang 18.1.8, while the same tests pass with
@@ -19,7 +19,6 @@
#undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL #undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL
#undef JSON_INLINE_VARIABLE #undef JSON_INLINE_VARIABLE
#undef JSON_NO_UNIQUE_ADDRESS #undef JSON_NO_UNIQUE_ADDRESS
#undef JSON_INTERNAL_ALWAYS_INLINE
#undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION #undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_DTOA_SSE2 #undef JSON_DTOA_SSE2
-1
View File
@@ -13,7 +13,6 @@
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t #include <cstdint> // uint8_t, uint32_t
#include <string> // string, to_string #include <string> // string, to_string
#include <utility> // move
#include <nlohmann/detail/abi_macros.hpp> #include <nlohmann/detail/abi_macros.hpp>
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
+245 -34
View File
@@ -155,7 +155,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
friend class ::nlohmann::detail::iter_impl; friend class ::nlohmann::detail::iter_impl;
template<typename BasicJsonType, typename CharType, typename OutputSinkType> template<typename BasicJsonType, typename CharType, typename OutputSinkType>
friend class ::nlohmann::detail::binary_writer; friend class ::nlohmann::detail::binary_writer;
template<typename BasicJsonType, typename InputType, typename SAX, bool AllowRecovery> template<typename BasicJsonType, typename InputType, typename SAX>
friend class ::nlohmann::detail::binary_reader; friend class ::nlohmann::detail::binary_reader;
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType>
friend class ::nlohmann::detail::json_sax_dom_parser; friend class ::nlohmann::detail::json_sax_dom_parser;
@@ -6114,7 +6114,7 @@ public:
auto ia = detail::input_adapter(std::forward<InputType>(i)); auto ia = detail::input_adapter(std::forward<InputType>(i));
return format == input_format_t::json return format == input_format_t::json
? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict)
: detail::binary_reader<basic_json, decltype(ia), SAX, true>(std::move(ia), format).sax_parse(sax, strict, tag_handler); : detail::binary_reader<basic_json, decltype(ia), SAX>(std::move(ia), format).sax_parse(sax, strict, tag_handler);
} }
/// @brief generate SAX events (iterator pair, or iterator+sentinel pair for C++20 ranges support) /// @brief generate SAX events (iterator pair, or iterator+sentinel pair for C++20 ranges support)
@@ -6132,7 +6132,7 @@ public:
auto ia = detail::input_adapter(std::move(first), std::move(last)); auto ia = detail::input_adapter(std::move(first), std::move(last));
return format == input_format_t::json return format == input_format_t::json
? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict)
: detail::binary_reader<basic_json, decltype(ia), SAX, true>(std::move(ia), format).sax_parse(sax, strict, tag_handler); : detail::binary_reader<basic_json, decltype(ia), SAX>(std::move(ia), format).sax_parse(sax, strict, tag_handler);
} }
/// @brief generate SAX events /// @brief generate SAX events
@@ -6171,7 +6171,7 @@ public:
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict) ? parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict)
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
: detail::binary_reader<basic_json, decltype(ia), SAX, true>(std::move(ia), format).sax_parse(sax, strict, tag_handler); : detail::binary_reader<basic_json, decltype(ia), SAX>(std::move(ia), format).sax_parse(sax, strict, tag_handler);
} }
#endif #endif
#if defined(__clang__) #if defined(__clang__)
@@ -7066,6 +7066,125 @@ public:
detail::throw_patch_not_an_array(json_patch); detail::throw_patch_not_an_array(json_patch);
} }
// the usual patch operations "add", "remove", "replace" and "test" on
// a pointer without escapes that denotes existing array elements and
// object members (as "add" does for the parent of the new value), as
// a diff creates them: apply the operation and return true; return
// false, with nothing changed, for all others
string_t token;
const string_t value_key = "value";
const auto try_plain = [&result, &token, &value_key](patch_operations operation, const string_t& path, const basic_json & val)
{
if (path.empty() || path.find_first_of('~') != string_t::npos)
{
return false;
}
// a missing "value" is reported below
const auto value_it = val.m_data.m_value.object->find(value_key);
const basic_json& value = value_it != val.m_data.m_value.object->end() ? value_it->second : val;
if (operation != patch_operations::remove && value_it == val.m_data.m_value.object->end())
{
return false;
}
switch (operation)
{
case patch_operations::replace:
{
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
if (target == nullptr)
{
return false;
}
*target = value;
return true;
}
case patch_operations::test:
{
const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
// a failing test is reported below
return target != nullptr && *target == value;
}
case patch_operations::add:
case patch_operations::remove:
{
// the parent of the pointer and its last reference token
std::size_t slash = path.size();
while (slash > 0 && path[slash - 1] != '/')
{
--slash;
}
if (slash == 0)
{
return false;
}
--slash;
basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token);
if (parent == nullptr)
{
return false;
}
token.clear();
token.append(path.data() + slash + 1, path.size() - slash - 1);
if (operation == patch_operations::add)
{
if (parent->is_object())
{
(*parent)[token] = value;
return true;
}
if (parent->is_array())
{
typename basic_json::size_type idx{};
if (token == "-")
{
parent->push_back(value);
return true;
}
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size())
{
parent->insert(parent->begin() + static_cast<difference_type>(idx), value);
return true;
}
}
return false;
}
if (parent->is_object())
{
const auto it = parent->find(token);
if (it == parent->end())
{
return false;
}
parent->erase(it);
return true;
}
if (parent->is_array())
{
typename basic_json::size_type idx{};
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx < parent->size())
{
parent->erase(idx);
return true;
}
}
return false;
}
case patch_operations::move:
case patch_operations::copy:
case patch_operations::invalid:
default:
return false;
}
};
// iterate and apply the operations // iterate and apply the operations
for (const auto& val : json_patch) for (const auto& val : json_patch)
{ {
@@ -7102,9 +7221,19 @@ public:
// collect mandatory members // collect mandatory members
const string_t& op = *get_value("op", "op", true).m_data.m_value.string; const string_t& op = *get_value("op", "op", true).m_data.m_value.string;
const string_t& path = *get_value(op, "path", true).m_data.m_value.string; const string_t& path = *get_value(op, "path", true).m_data.m_value.string;
const patch_operations operation = get_op(op);
// the usual operations on a plain pointer are applied without
// building the pointer's reference tokens; anything else (and
// every error) is handled below
if (try_plain(operation, path, val))
{
continue;
}
json_pointer ptr(path); json_pointer ptr(path);
switch (get_op(op)) switch (operation)
{ {
case patch_operations::add: case patch_operations::add:
{ {
@@ -7212,7 +7341,8 @@ public:
{ {
// the patch // the patch
basic_json result(value_t::array); basic_json result(value_t::array);
diff_recursively(result, source, target, path, 0); string_t buffer = path;
diff_recursively(result, source, target, buffer, 0);
return result; return result;
} }
@@ -7256,31 +7386,69 @@ public:
// function it appears in. In diff_recursively, which is on the call stack // function it appears in. In diff_recursively, which is on the call stack
// once per nesting level, that made every level cost kilobytes of stack. // once per nesting level, that made every level cost kilobytes of stack.
/// @brief append an operation @a op for @a path to @a result, with @a value if it is not null
static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value)
{
basic_json entry(value_t::object);
entry["op"] = op;
entry["path"] = path;
if (value != nullptr)
{
entry["value"] = *value;
}
result.push_back(std::move(entry));
}
/// @brief append a "replace" operation for @a path with @a value to @a result /// @brief append a "replace" operation for @a path with @a value to @a result
static void diff_replace(basic_json& result, const string_t& path, const basic_json& value) static void diff_replace(basic_json& result, const string_t& path, const basic_json& value)
{ {
result.push_back( diff_operation(result, "replace", path, &value);
{
{"op", "replace"}, {"path", path}, {"value", value}
});
} }
/// @brief append a "remove" operation for @a path to @a result /// @brief append a "remove" operation for @a path to @a result
static void diff_remove(basic_json& result, const string_t& path) static void diff_remove(basic_json& result, const string_t& path)
{ {
result.push_back(object( diff_operation(result, "remove", path, nullptr);
{
{"op", "remove"}, {"path", path}
}));
} }
/// @brief append an "add" operation for @a path with @a value to @a result /// @brief append an "add" operation for @a path with @a value to @a result
static void diff_add(basic_json& result, const string_t& path, const basic_json& value) static void diff_add(basic_json& result, const string_t& path, const basic_json& value)
{ {
result.push_back( diff_operation(result, "add", path, &value);
{ }
{"op", "add"}, {"path", path}, {"value", value}
}); /// @brief append "/" and the decimal representation of @a index to @a path
static void diff_append_index(string_t& path, std::size_t index)
{
path.push_back('/');
json_pointer::append_index(path, index);
}
/// @brief append "/" and the escaped @a key to @a path
static void diff_append_key(string_t& path, const string_t& key)
{
path.push_back('/');
detail::escape_into(path, key);
}
/// @brief the path of the member @a key of the value at @a path
static string_t diff_key_path(const string_t& path, const string_t& key)
{
string_t result;
result.reserve(path.size() + key.size() + 1);
result.append(path.data(), path.size());
diff_append_key(result, key);
return result;
}
/// @brief the path of the element @a index of the value at @a path
static string_t diff_index_path(const string_t& path, const std::size_t index)
{
string_t result;
result.reserve(path.size() + 8);
result.append(path.data(), path.size());
diff_append_index(result, index);
return result;
} }
/// @brief append the "remove" operations for the elements of array /// @brief append the "remove" operations for the elements of array
@@ -7293,13 +7461,18 @@ public:
// in that order avoids the quadratic reinsertion done before // in that order avoids the quadratic reinsertion done before
for (std::size_t j = source.size(); j > index; --j) for (std::size_t j = source.size(); j > index; --j)
{ {
diff_remove(result, detail::concat<string_t>(path, '/', detail::to_string<string_t>(j - 1))); diff_remove(result, diff_index_path(path, j - 1));
} }
// add other remaining elements // add other remaining elements
for (std::size_t i = source.size(); i < target.size(); ++i) if (source.size() < target.size())
{ {
diff_add(result, detail::concat<string_t>(path, "/-"), target[i]); string_t tail_path = path;
tail_path.append("/-", 2);
for (std::size_t i = source.size(); i < target.size(); ++i)
{
diff_add(result, tail_path, target[i]);
}
} }
} }
@@ -7308,7 +7481,7 @@ public:
{ {
for (const auto& it : added) for (const auto& it : added)
{ {
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value()); diff_add(result, diff_key_path(path, it.key()), it.value());
} }
} }
@@ -7329,10 +7502,25 @@ public:
std::vector<const_iterator>& added) std::vector<const_iterator>& added)
{ {
// first pass: one lookup per source key // first pass: one lookup per source key
// Objects of the same size usually have the same keys, in the same
// order: walk them together as long as that holds, which takes no lookups.
target_values.reserve(source.size()); target_values.reserve(source.size());
std::size_t common = 0; std::size_t common = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it) bool together = source.size() == target.size();
for (auto it = source.cbegin(), partner = target.cbegin(); it != source.cend(); ++it)
{ {
if (together)
{
if (it.key() == partner.key())
{
target_values.push_back(&partner.value());
++common;
++partner;
continue;
}
together = false;
}
const auto found = target.find(it.key()); const auto found = target.find(it.key());
if (found != target.cend()) if (found != target.cend())
{ {
@@ -7345,6 +7533,12 @@ public:
} }
} }
// the same keys in the same order
if (together)
{
return true;
}
// second pass: find the keys only target has. An object type that // second pass: find the keys only target has. An object type that
// keeps its members in insertion order, such as the one backing // keeps its members in insertion order, such as the one backing
// ordered_json, also needs the common keys to be in the same order // ordered_json, also needs the common keys to be in the same order
@@ -7410,6 +7604,13 @@ public:
return true; return true;
} }
// equal objects, whose members are in a different order, need no
// operations
if (source == target)
{
return false;
}
// slow path: the common keys are in a different relative // slow path: the common keys are in a different relative
// order in source and target (only possible for a // order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a // reorderable object_t like ordered_map). Building a
@@ -7426,7 +7627,7 @@ public:
// it moves it to the end, fixing its position. // it moves it to the end, fixing its position.
for (auto it = source.cbegin(); it != source.cend(); ++it) for (auto it = source.cbegin(); it != source.cend(); ++it)
{ {
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key()))); diff_remove(result, diff_key_path(path, it.key()));
} }
// add every key that is either common (just removed // add every key that is either common (just removed
@@ -7435,7 +7636,7 @@ public:
// target exactly // target exactly
for (auto it = target.cbegin(); it != target.cend(); ++it) for (auto it = target.cbegin(); it != target.cend(); ++it)
{ {
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value()); diff_add(result, diff_key_path(path, it.key()), it.value());
} }
return false; return false;
} }
@@ -7444,6 +7645,9 @@ public:
@brief @ref diff, for values at nesting level @a depth, appending the @brief @ref diff, for values at nesting level @a depth, appending the
operations to @a result operations to @a result
@a path is the path of the values; it is a buffer that is extended and
shortened again below, and is the same when the function returns.
Diffing two arrays or objects calls this function again, once per nesting Diffing two arrays or objects calls this function again, once per nesting
level, so values nested deeply enough used to exhaust the call stack and level, so values nested deeply enough used to exhaust the call stack and
terminate the process. The descent is bounded here: once @ref terminate the process. The descent is bounded here: once @ref
@@ -7451,7 +7655,7 @@ public:
diff_iteratively diffs what is left without the call stack. diff_iteratively diffs what is left without the call stack.
*/ */
static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target, static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth) string_t& path, const std::size_t depth)
{ {
// if the values are the same, there is nothing to do // if the values are the same, there is nothing to do
if (source == target) if (source == target)
@@ -7463,9 +7667,9 @@ public:
} }
/// @brief @ref diff_recursively for values known to be unequal; callers /// @brief @ref diff_recursively for values known to be unequal; callers
/// compare first so they build no path for equal ones /// compare first so they extend the path only for unequal ones
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target, static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth) string_t& path, const std::size_t depth)
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{ {
@@ -7484,6 +7688,8 @@ public:
{ {
case value_t::array: case value_t::array:
{ {
const std::size_t path_length = path.size();
// first pass: traverse common elements // first pass: traverse common elements
std::size_t i = 0; std::size_t i = 0;
while (i < source.size() && i < target.size()) while (i < source.size() && i < target.size())
@@ -7491,7 +7697,9 @@ public:
// recursive call to compare array values at index i // recursive call to compare array values at index i
if (!(source[i] == target[i])) if (!(source[i] == target[i]))
{ {
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1); diff_append_index(path, i);
diff_unequal(result, source[i], target[i], path, depth + 1);
path.resize(path_length);
} }
++i; ++i;
} }
@@ -7513,6 +7721,7 @@ public:
// lookup is needed. Deleted keys are interleaved here // lookup is needed. Deleted keys are interleaved here
// too, in source's original order, to match the // too, in source's original order, to match the
// historical (pre-reordering-aware) output order. // historical (pre-reordering-aware) output order.
const std::size_t path_length = path.size();
std::size_t i = 0; std::size_t i = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it, ++i) for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
{ {
@@ -7520,13 +7729,15 @@ public:
{ {
if (!(it.value() == *target_values[i])) if (!(it.value() == *target_values[i]))
{ {
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1); diff_append_key(path, it.key());
diff_unequal(result, it.value(), *target_values[i], path, depth + 1);
path.resize(path_length);
} }
} }
else else
{ {
// found a key that is not in target -> remove it // found a key that is not in target -> remove it
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key()))); diff_remove(result, diff_key_path(path, it.key()));
} }
} }
@@ -7657,7 +7868,7 @@ public:
if (i < source_array.size() && i < target_array.size()) if (i < source_array.size() && i < target_array.size())
{ {
++stack.back().index; ++stack.back().index;
detail::concat_into(current_path, '/', detail::to_string<string_t>(i)); diff_append_index(current_path, i);
enter(source_array[i], target_array[i]); enter(source_array[i], target_array[i]);
if (stack.size() == depth) if (stack.size() == depth)
{ {
@@ -7680,7 +7891,7 @@ public:
if (target_value_ptr != nullptr) if (target_value_ptr != nullptr)
{ {
const basic_json& target_value = *target_value_ptr; const basic_json& target_value = *target_value_ptr;
detail::concat_into(current_path, '/', detail::escape(it.key())); diff_append_key(current_path, it.key());
enter(it.value(), target_value); enter(it.value(), target_value);
if (stack.size() == depth) if (stack.size() == depth)
{ {
@@ -7690,7 +7901,7 @@ public:
else else
{ {
// found a key that is not in target -> remove it // found a key that is not in target -> remove it
diff_remove(result, detail::concat<string_t>(current_path, '/', detail::escape(it.key()))); diff_remove(result, diff_key_path(current_path, it.key()));
} }
continue; continue;
} }
+715 -2624
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -47,7 +47,6 @@ inline namespace json_literals
namespace detail namespace detail
{ {
using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_callback_parser; using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_callback_parser;
using NLOHMANN_JSON_NAMESPACE::detail::json_sax_dom_parser;
using NLOHMANN_JSON_NAMESPACE::detail::unknown_size; using NLOHMANN_JSON_NAMESPACE::detail::unknown_size;
} // namespace detail } // namespace detail
-13
View File
@@ -47,10 +47,6 @@ dumps is stable under exactly the same values that break operator==.
The unit tests run the same checks on a fixed corpus (see the "BJData round-trip The unit tests run the same checks on a fixed corpus (see the "BJData round-trip
invariants" test case), so keep both in sync. invariants" test case), so keep both in sync.
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_bjdata() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -61,7 +57,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -86,9 +81,6 @@ static bool is_value_stable(const json& lhs, const json& rhs)
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::bjdata).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -107,9 +99,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2.1: round trip without adding size annotations to container types // step 2.1: round trip without adding size annotations to container types
@@ -143,7 +132,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -154,7 +142,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_bon8(vec) - j2 = from_bon8(vec)
- assert(to_bon8(j2) == vec) - assert(to_bon8(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_bon8() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_bon8
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::bon8).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_bson(vec) - j2 = from_bson(vec)
- assert(to_bson(j2) == vec) - assert(to_bson(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_bson() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_bson
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::bson).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors can occur during parsing, too // out of range errors can occur during parsing, too
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_cbor(vec) - j2 = from_cbor(vec)
- assert(to_cbor(j2) == vec) - assert(to_cbor(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_cbor() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_cbor
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::cbor).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors can occur during parsing, too // out of range errors can occur during parsing, too
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-12
View File
@@ -24,16 +24,11 @@ array data, it performs the following steps:
The pointer range takes the bulk fast paths, the other input adapters read byte The pointer range takes the bulk fast paths, the other input adapters read byte
by byte; see check_input_adapters(). by byte; see check_input_adapters().
Furthermore, it parses data with a SAX parser that recovers from every error
and checks that the events are balanced, that parsing ends, and that valid
input is parsed without errors (see #3989).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers. drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -64,13 +59,6 @@ struct accept_json
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
{
const auto checker = check_recovering_parse(data, size, json::input_format_t::json);
assert(checker.events <= (4 * size) + 4);
assert((checker.errors == 0) == json::accept(data, data + size));
}
// step 0: parse input without exceptions // step 0: parse input without exceptions
bool noexcept_threw = false; bool noexcept_threw = false;
json const j_noexcept = parse_without_exceptions([&] { return json::parse(data, data + size, nullptr, false); }, noexcept_threw); json const j_noexcept = parse_without_exceptions([&] { return json::parse(data, data + size, nullptr, false); }, noexcept_threw);
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_msgpack(vec) - j2 = from_msgpack(vec)
- assert(to_msgpack(j2) == vec) - assert(to_msgpack(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_msgpack() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_msgpack
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::msgpack).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -26,10 +26,6 @@ array data, it performs the following steps:
The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip
invariants" test case), so keep both in sync. invariants" test case), so keep both in sync.
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_ubjson() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -40,7 +36,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -58,9 +53,6 @@ struct read_ubjson
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::ubjson).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -79,9 +71,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2.1: round trip without adding size annotations to container types // step 2.1: round trip without adding size annotations to container types
@@ -113,7 +102,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -124,7 +112,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-154
View File
@@ -1,154 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
namespace
{
// a SAX parser that recovers from every error and checks that the events are
// balanced and that every key is followed by exactly one value
class recovering_checker : public nlohmann::json_sax<nlohmann::json>
{
public:
bool null() override
{
return value();
}
bool boolean(bool /*val*/) override
{
return value();
}
bool number_integer(number_integer_t /*val*/) override
{
return value();
}
bool number_unsigned(number_unsigned_t /*val*/) override
{
return value();
}
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
{
return value();
}
bool string(string_t& /*val*/) override
{
return value();
}
bool binary(binary_t& /*val*/) override
{
return value();
}
bool start_object(std::size_t /*elements*/) override
{
value();
stack.push_back('o');
return true;
}
bool key(string_t& /*val*/) override
{
++events;
assert(!stack.empty() && stack.back() == 'o');
stack.back() = 'v';
return true;
}
bool end_object() override
{
++events;
assert(!stack.empty() && stack.back() == 'o');
stack.pop_back();
return true;
}
bool start_array(std::size_t /*elements*/) override
{
value();
stack.push_back('a');
return true;
}
bool end_array() override
{
++events;
assert(!stack.empty() && stack.back() == 'a');
stack.pop_back();
return true;
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
{
++errors;
return true;
}
bool complete() const
{
return stack.empty();
}
std::size_t events = 0;
std::size_t errors = 0;
private:
bool value()
{
++events;
if (!stack.empty())
{
// an array element, or the value of a key
assert(stack.back() != 'o');
if (stack.back() == 'v')
{
stack.back() = 'o';
}
}
return true;
}
// 'a' for an array, 'o' for an object that expects a key, 'v' for an
// object that expects the value of a key
std::vector<char> stack {}; // NOLINT(readability-redundant-member-init)
};
/// parses @a data with a recovering_checker from memory and from a stream,
/// checks that both see the same, that the events are balanced, and that the
/// number of errors is bounded, and returns the checker (see #3989)
inline recovering_checker check_recovering_parse(const std::uint8_t* data, const std::size_t size, const nlohmann::json::input_format_t format)
{
recovering_checker checker;
const bool ok = nlohmann::json::sax_parse(data, data + size, &checker, format);
assert(checker.complete());
assert(checker.errors <= size + 1);
assert(ok == (checker.errors == 0));
std::istringstream stream(std::string(reinterpret_cast<const char*>(data), size));
recovering_checker stream_checker;
assert(nlohmann::json::sax_parse(stream, &stream_checker, format) == ok);
assert(stream_checker.complete());
assert(stream_checker.events == checker.events);
assert(stream_checker.errors == checker.errors);
return checker;
}
} // namespace
+1 -3
View File
@@ -131,13 +131,11 @@ struct SaxEventLogger : public nlohmann::json_sax<nlohmann::json>
{ {
errored = true; errored = true;
events.push_back("parse_error(" + std::to_string(position) + ")"); events.push_back("parse_error(" + std::to_string(position) + ")");
return recover; return false;
} }
std::vector<std::string> events {}; // NOLINT(readability-redundant-member-init) std::vector<std::string> events {}; // NOLINT(readability-redundant-member-init)
bool errored = false; bool errored = false;
/// whether parse_error() asks the parser to recover from the error (see #3989)
bool recover = false;
}; };
struct SaxEventLoggerExitAfterStartObject : public SaxEventLogger struct SaxEventLoggerExitAfterStartObject : public SaxEventLogger
-40
View File
@@ -415,46 +415,6 @@ TEST_CASE("alternative string type")
CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})"); CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})");
} }
SECTION("error recovery")
{
// a SAX parser that recovers from every error (see #3989)
struct recovering_parser : nlohmann::detail::json_sax_dom_parser<alt_json>
{
explicit recovering_parser(alt_json& j)
: nlohmann::detail::json_sax_dom_parser<alt_json>(j, false)
{}
// sax_parse() calls the SAX parser's own parse_error(), so hiding
// the one of the base class is what recovering takes
// NOLINTNEXTLINE(bugprone-derived-method-shadowing-base-method)
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const nlohmann::detail::exception& /*unused*/)
{
++errors;
return true;
}
std::size_t errors = 0;
};
alt_json j;
recovering_parser sax(j);
// not inside CHECK(): MSVC reads the escape in a stringized raw string
const std::string input = R"([1., "a\qb", tru, {"k" 2}])";
CHECK(!alt_json::sax_parse(input, &sax));
CHECK(sax.errors == 4);
CHECK(j.dump() == R"([1,"aqb",null,{"k":2}])");
// a UBJSON high-precision number, a CBOR key that is not a string
alt_json u;
recovering_parser ubjson_sax(u);
CHECK(!alt_json::sax_parse(std::vector<std::uint8_t> {'[', 'H', 'i', 2, '1', '.', ']'}, &ubjson_sax, alt_json::input_format_t::ubjson));
CHECK(u.dump() == "[1]");
alt_json c;
recovering_parser cbor_sax(c);
CHECK(!alt_json::sax_parse(std::vector<std::uint8_t> {0xA2, 0x01, 0x02, 0x61, 'a', 0x03}, &cbor_sax, alt_json::input_format_t::cbor));
CHECK(c.dump() == R"({"a":3})");
}
SECTION("conversion between basic_json specializations (#2649)") SECTION("conversion between basic_json specializations (#2649)")
{ {
// explicit conversions are always possible // explicit conversions are always possible
-645
View File
@@ -2847,648 +2847,3 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX")
} }
} }
#endif #endif
namespace
{
/// builds a value like json::parse(), but asks the parser to recover from
/// errors (see #3989), and checks that the events it receives are balanced
class RecoveringDomParser
{
public:
explicit RecoveringDomParser(json& j, std::size_t max_errors_ = static_cast<std::size_t>(-1))
: dom(j, false)
, max_errors(max_errors_)
{}
bool null()
{
value();
return dom.null();
}
bool boolean(bool val)
{
value();
return dom.boolean(val);
}
bool number_integer(json::number_integer_t val)
{
value();
return dom.number_integer(val);
}
bool number_unsigned(json::number_unsigned_t val)
{
value();
return dom.number_unsigned(val);
}
bool number_float(json::number_float_t val, const std::string& s)
{
value();
return dom.number_float(val, s);
}
bool string(std::string& val)
{
value();
return dom.string(val);
}
bool binary(json::binary_t& val)
{
value();
return dom.binary(val);
}
bool start_object(std::size_t elements)
{
value();
stack.push_back('o');
return dom.start_object(elements);
}
bool key(std::string& val)
{
++events;
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.back() = 'v';
return dom.key(val);
}
bool end_object()
{
++events;
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_object();
}
bool start_array(std::size_t elements)
{
value();
stack.push_back('a');
return dom.start_array(elements);
}
bool end_array()
{
++events;
if (stack.empty() || stack.back() != 'a')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_array();
}
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& ex)
{
errors.emplace_back(ex.what());
return errors.size() < max_errors;
}
/// whether the events were balanced and every key was followed by a value
bool balanced() const
{
return well_formed && stack.empty();
}
/// builds the value
nlohmann::detail::json_sax_dom_parser<json> dom;
std::vector<std::string> errors {}; // NOLINT(readability-redundant-member-init)
std::size_t events = 0;
/// the open containers: 'a' for an array, 'o' for an object that expects
/// a key, 'v' for an object that expects the value of a key
std::vector<char> stack {}; // NOLINT(readability-redundant-member-init)
bool well_formed = true;
std::size_t max_errors;
private:
/// a value is passed: it is an array element, or the value of a key
void value()
{
++events;
if (!stack.empty())
{
if (stack.back() == 'v')
{
stack.back() = 'o';
}
else if (stack.back() == 'o')
{
// a value without a key
well_formed = false;
}
}
}
};
struct RecoveryResult
{
json value;
std::vector<std::string> errors;
std::size_t events;
bool ok;
bool balanced;
};
template<typename InputType>
RecoveryResult parse_recovering(InputType&& input, const bool strict = true,
const bool ignore_comments = false, const bool ignore_trailing_commas = false)
{
json j;
RecoveringDomParser sax(j);
const bool ok = json::sax_parse(std::forward<InputType>(input), &sax, json::input_format_t::json,
strict, ignore_comments, ignore_trailing_commas);
return {j, sax.errors, sax.events, ok, sax.balanced()};
}
/// stops after a number of events, but recovers from errors
class RecoveringCountdown
{
public:
explicit RecoveringCountdown(const int count)
: countdown(count)
{}
bool null()
{
return countdown.null();
}
bool boolean(bool val)
{
return countdown.boolean(val);
}
bool number_integer(json::number_integer_t val)
{
return countdown.number_integer(val);
}
bool number_unsigned(json::number_unsigned_t val)
{
return countdown.number_unsigned(val);
}
bool number_float(json::number_float_t val, const std::string& s)
{
return countdown.number_float(val, s);
}
bool string(std::string& val)
{
return countdown.string(val);
}
bool binary(json::binary_t& val)
{
return countdown.binary(val);
}
bool start_object(std::size_t elements)
{
return countdown.start_object(elements);
}
bool key(std::string& val)
{
return countdown.key(val);
}
bool end_object()
{
return countdown.end_object();
}
bool start_array(std::size_t elements)
{
return countdown.start_array(elements);
}
bool end_array()
{
return countdown.end_array();
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) // NOLINT(readability-convert-member-functions-to-static)
{
return true;
}
private:
SaxCountdown countdown;
};
/// a repaired input: the value it is repaired to, and the number of errors
struct Repair
{
const char* input;
const char* expected;
std::size_t errors;
};
} // namespace
TEST_CASE("parser error recovery (#3989)")
{
SECTION("repairs")
{
const std::vector<Repair> repairs =
{
// a missing separator is inserted
{"[1 2]", "[1,2]", 1},
{R"({"a":1 "b":2})", R"({"a":1,"b":2})", 1},
{R"({"a" 1})", R"({"a":1})", 1},
{"[1 tru 2]", "[1,null,2]", 2},
{R"({"a" "b": 1})", R"({"a":"b"})", 2},
// a missing value is null in an object; in an array, a ',' stands
// for null, while an array that ends there just ends
{R"({"a":})", R"({"a":null})", 1},
{R"({"a"})", R"({"a":null})", 1},
{R"({"a","b":1})", R"({"a":null,"b":1})", 1},
{"[1,,2]", "[1,null,2]", 1},
{"[,1]", "[null,1]", 1},
{"[1,]", "[1]", 1},
{"[1,2,3,]", "[1,2,3]", 1},
{R"({"a":1,})", R"({"a":1})", 1},
// a broken string keeps what can be read
{R"(["a\qb"])", R"(["aqb"])", 1},
{R"({"na\me":1})", R"({"name":1})", 1},
{"[\"\xFF\"]", R"(["\uFFFD"])", 1},
{"[\"a\xC3(\"]", R"(["a\uFFFD("])", 1},
{"[\"\xE2\x82\"]", R"(["\uFFFD"])", 1},
{"[\"\xC3\\\\\", 1]", R"(["\uFFFD\\",1])", 1},
{R"(["\u12"])", R"(["\uFFFD"])", 1},
{R"(["\u12G4"])", R"(["\uFFFDG4"])", 1},
{R"(["\uDC00x"])", R"(["\uFFFDx"])", 1},
{R"(["\uD800x"])", R"(["\uFFFDx"])", 1},
{R"(["\uD800\u0041"])", R"(["\uFFFDA"])", 1},
{R"(["\uD800\uD800\uDC00"])", R"(["\uFFFD\uD800\uDC00"])", 1},
{R"(["\uD800\uD800\uD800x"])", R"(["\uFFFD\uFFFD\uFFFDx"])", 1},
{
R"(["\uD800\"x", 1])", R"(["\uFFFD\"x",1])", 1
},
{R"(["\uD800\q"])", R"(["\uFFFDq"])", 1},
{"[\"a\tb\"]", R"(["a\tb"])", 1},
{R"(["a\qb\u0041\x"])", R"(["aqbAx"])", 1},
// a broken number keeps its longest valid prefix
{"[1.]", "[1]", 1},
{"[-2.]", "[-2]", 1},
{"[1.5e]", "[1.5]", 1},
{"[1e+]", "[1]", 1},
{"[1.x2, 3]", "[1,3]", 1},
// what cannot be read at all is null
{"[1,NaN,3]", "[1,null,3]", 1},
{"[tru]", "[null]", 1},
{"[-]", "[null]", 1},
{R"({"a":Infinity})", R"({"a":null})", 1},
// a stray token is dropped
{"[:1]", "[1]", 1},
{R"(["a":1])", R"(["a",1])", 1},
{R"({"a"::1})", R"({"a":1})", 1},
// a member that cannot be read is skipped
{R"({1:2,"b":3})", R"({"b":3})", 1},
{R"({"a":1 2})", R"({"a":1})", 1},
{R"({,"a":1})", R"({"a":1})", 1},
{R"({"a":1,,"b":2})", R"({"a":1,"b":2})", 1},
{"{a:1}", "{}", 1},
{R"({"a":1 [1,{"b":2}], "c":3})", R"({"a":1,"c":3})", 1},
{R"([{1}, "a"])", R"([{},"a"])", 1},
// a wrong closing bracket closes the innermost container
{R"({"a":[1,2}, "b":3})", R"({"a":[1,2],"b":3})", 1},
{R"([{"a":1], 2])", R"([{"a":1},2])", 1},
{"{]", "{}", 1},
{"[}", "[]", 1},
// the end of the input closes all containers
{R"({"a":[1,2)", R"({"a":[1,2]})", 1},
{"[", "[]", 1},
{"{", "{}", 1},
{R"({"a")", R"({"a":null})", 1},
{R"({"a":)", R"({"a":null})", 1},
{"[1,", "[1]", 1},
{"[[[1", "[[[1]]]", 1},
{
R"(["abc)", R"(["abc"])", 2
},
{"[1,tr", "[1,null]", 2},
{"\"abc", "\"abc\"", 1},
{"[\"ab\ncd\"]", R"(["ab",null,"]"])", 4},
// what comes before the top-level value is skipped
{")]}'\n{\"a\":1}", R"({"a":1})", 1},
{R"(data: {"a":1})", R"({"a":1})", 1},
{"\xEF\xBB[1]", "[1]", 1},
// what comes after it is an error that ends parsing
{R"({"a":1}})", R"({"a":1})", 1},
{"[1}]", "[1]", 2},
{"[1] [2]", "[1]", 1},
};
for (const auto& repair : repairs)
{
CAPTURE(repair.input)
const auto result = parse_recovering(std::string(repair.input));
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.value == json::parse(repair.expected));
CHECK(result.errors.size() == repair.errors);
}
}
SECTION("number overflow")
{
const auto result = parse_recovering(std::string("[1e999,-1e999]"));
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.errors.size() == 2);
CHECK(result.errors[0] == "[json.exception.out_of_range.406] number overflow parsing '1e999'");
REQUIRE(result.value.size() == 2);
CHECK(result.value[0].is_number_float());
CHECK(result.value[0].get<double>() == std::numeric_limits<double>::infinity());
CHECK(result.value[1].get<double>() == -std::numeric_limits<double>::infinity());
// the SAX parser gets the number's text
SaxEventLogger logger;
logger.recover = true;
CHECK(!json::sax_parse("1e999", &logger));
CHECK(logger.events == std::vector<std::string>({"parse_error(5)", "number_float(1e999)"}));
}
SECTION("nothing to recover")
{
for (const std::string s :
{
"", " ", "]", "tru", "NaN", ",:", "/* comment"
})
{
CAPTURE(s)
const auto result = parse_recovering(s, true, true);
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.events == 0);
CHECK(result.value == nullptr);
CHECK(result.errors.size() == 1);
}
}
SECTION("error messages")
{
// the first error is reported as without recovery
for (const std::string s :
{
"[1 2]", R"({"a":1 "b":2})", R"({"a" 1})", R"({"a":})", "[1,]", "[1.]",
R"(["a\qb"])", "[1e999]", "{1:2}", R"({"a":[1,2}})", "[1,", "[1] [2]", "{a:1}"
})
{
CAPTURE(s)
const auto result = parse_recovering(s);
REQUIRE(!result.errors.empty());
json _;
CHECK_THROWS_WITH_STD_STR(_ = json::parse(s), result.errors.front());
}
// the token of an error begins where the previous error was
const auto result = parse_recovering(std::string("[tru, fals, nul]"));
CHECK(result.errors == std::vector<std::string>(
{
"[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - invalid literal; last read: '[tru,'",
"[json.exception.parse_error.101] parse error at line 1, column 11: syntax error while parsing value - invalid literal; last read: ', fals,'",
"[json.exception.parse_error.101] parse error at line 1, column 16: syntax error while parsing value - invalid literal; last read: ', nul]'"
}));
CHECK(result.value == json::parse("[null,null,null]"));
}
SECTION("events")
{
// see #4522
SaxEventLogger logger;
logger.recover = true;
CHECK(!json::sax_parse(R"([{1}, "a"])", &logger));
CHECK(logger.events == std::vector<std::string>(
{
"start_array()", "start_object()", "parse_error(3)", "end_object()", "string(a)", "end_array()"
}));
}
SECTION("options")
{
SECTION("strict")
{
const auto result = parse_recovering(std::string("[1 2] [3]"), false);
CHECK(!result.ok);
CHECK(result.value == json::parse("[1,2]"));
CHECK(result.errors.size() == 1);
}
SECTION("ignore_trailing_commas")
{
for (const std::string s :
{
"[1,]", R"({"a":1,})", "[[1,],]"
})
{
CAPTURE(s)
const auto result = parse_recovering(s, true, false, true);
CHECK(result.ok);
CHECK(result.errors.empty());
}
auto result = parse_recovering(std::string("[1,,]"), true, false, true);
CHECK(result.value == json::parse("[1,null]"));
CHECK(result.errors.size() == 1);
result = parse_recovering(std::string(R"({"a":1,,})"), true, false, true);
CHECK(result.value == json::parse(R"({"a":1})"));
CHECK(result.errors.size() == 1);
}
SECTION("ignore_comments")
{
auto result = parse_recovering(std::string("[1 /* one */ 2]"), true, true);
CHECK(result.value == json::parse("[1,2]"));
CHECK(result.errors.size() == 1);
// a comment that is not closed runs to the end of the input, which
// is not reported again
result = parse_recovering(std::string("[1, 2 /* unterminated"), true, true);
CHECK(result.balanced);
CHECK(result.value == json::parse("[1,2]"));
CHECK(result.errors.size() == 1);
// a '/' that does not begin a comment is garbage
result = parse_recovering(std::string("[1, /x, 2]"), true, true);
CHECK(result.balanced);
CHECK(result.value == json::parse("[1,null,2]"));
CHECK(result.errors.size() == 1);
}
}
SECTION("null bytes")
{
// a null byte ends the input, unless JSON_STRICT_NUL_HANDLING is set
const auto result = parse_recovering(std::string("[1,\0x", 5));
CHECK(result.balanced);
CHECK(!result.ok);
#ifdef JSON_TEST_STRICT_NUL_HANDLING_ENABLED
CHECK(result.value == json::parse("[1,null]"));
#else
CHECK(result.value == json::parse("[1]"));
CHECK(result.errors.size() == 1);
#endif
const auto in_string = parse_recovering(std::string("[\"a\0b\"]", 7));
CHECK(in_string.balanced);
#ifdef JSON_TEST_STRICT_NUL_HANDLING_ENABLED
CHECK(in_string.value == json::array({std::string("a\0b", 3)}));
#else
CHECK(in_string.value == json::parse(R"(["a"])"));
#endif
}
SECTION("the SAX parser stops recovering")
{
json j;
RecoveringDomParser sax(j, 2);
CHECK(!json::sax_parse("[1 2 3 4 5]", &sax));
CHECK(sax.errors.size() == 2);
// an error at a delimiter that an invalid token consumed is reported
// to the SAX parser, too
json j2;
RecoveringDomParser sax2(j2, 2);
CHECK(!json::sax_parse("[tru}, 1]", &sax2));
CHECK(sax2.errors.size() == 2);
}
SECTION("an event stops parsing during a repair")
{
// start_object() and key() are passed, then null() for the missing
// value returns false
RecoveringCountdown countdown(2);
CHECK(!json::sax_parse(R"({"a":})", &countdown));
// the end of the input: end_array() for the second array returns false
RecoveringCountdown countdown2(4);
CHECK(!json::sax_parse("[[1", &countdown2));
}
SECTION("input adapters")
{
// the lexer reads contiguous and streaming input differently, and it
// puts back a character that ended an invalid token
for (const std::string s :
{
"[1 2]", "[tru}, 1]", R"({"a" "b\q", "c":[1.x, 2}})", "[\"\xFF\xC3(\", -, 1e+]", "{a:1,\"b\":2", ")]}' [1]"
})
{
CAPTURE(s)
const auto reference = parse_recovering(s);
CHECK(reference.balanced);
const auto from_c_string = parse_recovering(s.c_str());
CHECK(from_c_string.value == reference.value);
CHECK(from_c_string.errors == reference.errors);
const std::list<char> l(s.begin(), s.end());
json j;
RecoveringDomParser sax(j);
CHECK(!json::sax_parse(l.begin(), l.end(), &sax));
CHECK(j == reference.value);
CHECK(sax.errors == reference.errors);
std::istringstream ss(s);
const auto from_stream = parse_recovering(ss);
CHECK(from_stream.value == reference.value);
CHECK(from_stream.errors == reference.errors);
}
}
SECTION("long runs of errors")
{
// no error may copy all the input read before it
const auto closing = parse_recovering("[" + std::string(100000, '}'));
CHECK(closing.balanced);
CHECK(closing.value == json::array());
const auto garbage = parse_recovering("[" + std::string(100000, 'x') + "]");
CHECK(garbage.balanced);
CHECK(garbage.errors.size() == 1);
const auto commas = parse_recovering("{" + std::string(100000, ',') + "}");
CHECK(commas.balanced);
CHECK(commas.value == json::object());
}
SECTION("mutations of valid input")
{
// whatever the input, the events are balanced, every error is reported
// at most once, and valid input is parsed as usual
const std::vector<std::string> documents =
{
R"({"name": "value", "list": [1, -2.5, true, null, {"x": [[]]}], "e": "\u00e9"})",
R"([{"a": [1, 2, {"b": "c"}]}, [], {}, "\ud83d\ude00", 1e10])",
"{\"\xC3\xA9\": \"\xF0\x9F\x98\x80\"}",
R"( {"k" : [ "v" , 0 ] } )",
};
// each character that can be inserted, including a null byte
const std::string insertions("[]{},:\"x\\\0\xFF", 11);
std::vector<std::string> inputs;
for (const auto& doc : documents)
{
for (std::size_t i = 0; i <= doc.size(); ++i)
{
inputs.push_back(doc.substr(0, i));
if (i < doc.size())
{
inputs.push_back(doc.substr(0, i) + doc.substr(i + 1));
}
for (const char c : insertions)
{
inputs.push_back(doc.substr(0, i) + c + doc.substr(i));
}
}
}
for (const auto& s : inputs)
{
CAPTURE(s)
const auto result = parse_recovering(s);
CHECK(result.balanced);
CHECK(result.errors.size() <= s.size() + 1);
CHECK(result.events <= (4 * s.size()) + 4);
if (json::accept(s))
{
CHECK(result.ok);
CHECK(result.errors.empty());
CHECK(result.value == json::parse(s));
}
else
{
CHECK(!result.ok);
CHECK(!result.errors.empty());
}
}
}
}
+75
View File
@@ -31,6 +31,8 @@ using nlohmann::json;
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp" #include "round_trip_corpus.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#include "sax_countdown.hpp"
using utils::SaxCountdown;
TEST_CASE("MessagePack") TEST_CASE("MessagePack")
@@ -1643,6 +1645,30 @@ TEST_CASE("MessagePack")
} }
} }
} }
SECTION("SAX aborts")
{
SECTION("start_array(len)")
{
std::vector<uint8_t> const v = {0x93, 0x01, 0x02, 0x03};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("start_object(len)")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("key()")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(1);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
}
} }
TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays") TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays")
@@ -1713,6 +1739,21 @@ TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays")
CHECK(json::from_msgpack(packed) == j); CHECK(json::from_msgpack(packed) == j);
} }
} }
SECTION("a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_msgpack(j);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::msgpack));
}
} }
TEST_CASE("regression test - MessagePack ext type rejects a subtype that doesn't fit a single byte") TEST_CASE("regression test - MessagePack ext type rejects a subtype that doesn't fit a single byte")
@@ -1751,6 +1792,15 @@ TEST_CASE("MessagePack nesting does not consume the call stack")
CHECK(json::from_msgpack(input, true, false).is_discarded()); CHECK(json::from_msgpack(input, true, false).is_discarded());
} }
SECTION("a well-formed deep value is read through the SAX interface")
{
std::vector<uint8_t> input(300000, 0x91);
input.push_back(0x01); // innermost value
SaxCountdown accept_all(600001);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::msgpack));
}
SECTION("a well-formed deep value is read into a value") SECTION("a well-formed deep value is read into a value")
{ {
const std::size_t depth = 10000; const std::size_t depth = 10000;
@@ -1798,6 +1848,31 @@ TEST_CASE("MessagePack input that cannot be read is discarded by every overload"
#endif #endif
} }
TEST_CASE("MessagePack SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::msgpack))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_msgpack(j)) == 20);
}
TEST_CASE("single MessagePack roundtrip") TEST_CASE("single MessagePack roundtrip")
{ {
SECTION("sample.json") SECTION("sample.json")
+52
View File
@@ -22,6 +22,14 @@
// scoped enum, so get<std::byte>() (needed below to get<std::vector<std::byte>>() // scoped enum, so get<std::byte>() (needed below to get<std::vector<std::byte>>()
// from a plain JSON array, not just from an already-binary value) relies on // from a plain JSON array, not just from an already-binary value) relies on
// enum serialization being enabled // enum serialization being enabled
// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command
// line *before* including json.hpp, since the library #undefs it once the header
// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the
// tests of deprecated functions are skipped if these functions are deleted
#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1)
#define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
#endif
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1) #if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION #define SKIP_TESTS_FOR_ENUM_SERIALIZATION
#endif #endif
@@ -803,6 +811,50 @@ TEST_CASE("issue #5338 - truncated CBOR tagged binary subtype is rejected")
} }
} }
TEST_CASE("issue #5676 - SAX parsing of CBOR tags")
{
const json expected = json::binary({1, 2, 3}, 42);
const auto cbor = json::to_cbor(expected);
nlohmann::detail::json_sax_acceptor<json> acceptor;
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor));
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::error));
CHECK(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::ignore));
json parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(parsed);
CHECK(json::sax_parse(cbor, &sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(parsed == expected);
json iterator_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> iterator_sax(iterator_parsed);
CHECK(json::sax_parse(cbor.begin(), cbor.end(), &iterator_sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(iterator_parsed == expected);
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
json span_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> span_sax(span_parsed);
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(cbor.data(), cbor.size()), &span_sax,
json::input_format_t::cbor, true, false, false, json::cbor_tag_handler_t::store));
CHECK(span_parsed == expected);
#endif
const std::string text = "null";
CHECK(json::sax_parse(text, &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(json::sax_parse(text.begin(), text.end(), &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(text.data(), text.size()), &acceptor,
json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store));
#endif
}
TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with an object") TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with an object")
{ {
json t = {{"k", 1}}; json t = {{"k", 1}};
-768
View File
@@ -1,768 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
/////////////////////////////////////////////////////////////////////
// Tests that call basic_json::sax_parse have a file of their own: every
// sax_parse call instantiates the parser and binary reader that recover from
// errors (see #3989), and in unit-regression2.cpp, unit-regression3.cpp, and
// unit-msgpack.cpp this made the objects too large for the MinGW linker to
// relocate (see #5511).
/////////////////////////////////////////////////////////////////////
#include "doctest_compatibility.h"
// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command
// line *before* including json.hpp, since the library #undefs it once the header
// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the
// tests of deprecated functions are skipped if these functions are deleted
#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1)
#define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
#endif
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "sax_countdown.hpp"
using utils::SaxCountdown;
// a narrow number_float_t, so that a double read from binary input can
// overflow it
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
namespace
{
/// builds a value from SAX events, asks the parser to recover from its first
/// 100 errors, and checks that the events are balanced (see #3989)
template<typename BasicJsonType>
class BasicRecoveringParser
{
public:
explicit BasicRecoveringParser(BasicJsonType& j)
: dom(j, false)
{}
bool null()
{
value();
return dom.null();
}
bool boolean(bool val)
{
value();
return dom.boolean(val);
}
bool number_integer(typename BasicJsonType::number_integer_t val)
{
value();
return dom.number_integer(val);
}
bool number_unsigned(typename BasicJsonType::number_unsigned_t val)
{
value();
return dom.number_unsigned(val);
}
bool number_float(typename BasicJsonType::number_float_t val, const std::string& s)
{
value();
return dom.number_float(val, s);
}
bool string(std::string& val)
{
value();
return dom.string(val);
}
bool binary(typename BasicJsonType::binary_t& val)
{
value();
return dom.binary(val);
}
bool start_object(std::size_t elements)
{
value();
stack.push_back('o');
return dom.start_object(elements);
}
bool key(std::string& val)
{
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.back() = 'v';
return dom.key(val);
}
bool end_object()
{
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_object();
}
bool start_array(std::size_t elements)
{
value();
stack.push_back('a');
return dom.start_array(elements);
}
bool end_array()
{
if (stack.empty() || stack.back() != 'a')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_array();
}
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& ex)
{
messages.emplace_back(ex.what());
// a limit, so that a reader that does not stop fails the test
// instead of making it hang
return ++errors < 100;
}
/// whether the events were balanced and every key was followed by a value
bool balanced() const
{
return well_formed && stack.empty();
}
/// builds the value
nlohmann::detail::json_sax_dom_parser<BasicJsonType> dom;
std::size_t errors = 0;
std::vector<std::string> messages {}; // NOLINT(readability-redundant-member-init)
std::vector<char> stack {}; // NOLINT(readability-redundant-member-init)
bool well_formed = true;
private:
void value()
{
if (!stack.empty())
{
if (stack.back() == 'v')
{
stack.back() = 'o';
}
else if (stack.back() == 'o')
{
well_formed = false;
}
}
}
};
using RecoveringParser = BasicRecoveringParser<json>;
struct BinaryParseResult
{
json value;
std::size_t errors;
std::vector<std::string> messages;
bool ok;
bool balanced;
};
BinaryParseResult parse_binary_recovering(const std::vector<std::uint8_t>& input, const json::input_format_t format)
{
json j;
RecoveringParser sax(j);
const bool ok = json::sax_parse(input, &sax, format);
return {j, sax.errors, sax.messages, ok, sax.balanced()};
}
#if !defined(JSON_NOEXCEPTION)
/// the message of the exception that reading @a input into a JSON value
/// throws, or an empty string if reading succeeds
std::string binary_error_message(const std::vector<std::uint8_t>& input, const json::input_format_t format)
{
try
{
json _;
switch (format)
{
case json::input_format_t::cbor:
_ = json::from_cbor(input);
break;
case json::input_format_t::msgpack:
_ = json::from_msgpack(input);
break;
case json::input_format_t::ubjson:
_ = json::from_ubjson(input);
break;
case json::input_format_t::bjdata:
_ = json::from_bjdata(input);
break;
case json::input_format_t::bson:
_ = json::from_bson(input);
break;
case json::input_format_t::bon8:
_ = json::from_bon8(input);
break;
case json::input_format_t::json:
default:
break;
}
}
catch (const json::exception& e)
{
return e.what();
}
return "";
}
#endif
/// a BSON element: its type, its name, and its value
std::vector<std::uint8_t> bson_element(const std::uint8_t type, const std::string& name, const std::vector<std::uint8_t>& value)
{
std::vector<std::uint8_t> result = {type};
result.insert(result.end(), name.begin(), name.end());
result.push_back(0x00);
result.insert(result.end(), value.begin(), value.end());
return result;
}
/// a BSON document of the given elements; @a size_offset is added to the
/// size it declares
std::vector<std::uint8_t> bson_document(const std::vector<std::vector<std::uint8_t>>& elements, const int size_offset = 0)
{
std::vector<std::uint8_t> body;
for (const auto& element : elements)
{
body.insert(body.end(), element.begin(), element.end());
}
const auto size = static_cast<std::uint32_t>(static_cast<int>(body.size()) + 5 + size_offset);
std::vector<std::uint8_t> result = {static_cast<std::uint8_t>(size & 0xFFu), static_cast<std::uint8_t>((size >> 8u) & 0xFFu),
static_cast<std::uint8_t>((size >> 16u) & 0xFFu), static_cast<std::uint8_t>((size >> 24u) & 0xFFu)
};
result.insert(result.end(), body.begin(), body.end());
result.push_back(0x00);
return result;
}
/// a BSON int32 value
std::vector<std::uint8_t> bson_int32(const std::int32_t value)
{
const auto u = static_cast<std::uint32_t>(value);
return {static_cast<std::uint8_t>(u & 0xFFu), static_cast<std::uint8_t>((u >> 8u) & 0xFFu),
static_cast<std::uint8_t>((u >> 16u) & 0xFFu), static_cast<std::uint8_t>((u >> 24u) & 0xFFu)};
}
/// a BSON string value, whose length is @a length_offset off
std::vector<std::uint8_t> bson_string(const std::string& value, const std::int32_t length_offset = 0)
{
auto result = bson_int32(static_cast<std::int32_t>(value.size() + 1) + length_offset);
result.insert(result.end(), value.begin(), value.end());
result.push_back(0x00);
return result;
}
/// @a count bytes of value 0xAB
std::vector<std::uint8_t> bytes(const std::size_t count)
{
return std::vector<std::uint8_t>(count, 0xAB);
}
/// the bytes of @a parts, one after the other
std::vector<std::uint8_t> concatenated(std::initializer_list<std::vector<std::uint8_t>> parts)
{
std::vector<std::uint8_t> result;
for (const auto& part : parts)
{
result.insert(result.end(), part.begin(), part.end());
}
return result;
}
/// U+FFFD REPLACEMENT CHARACTER
std::string replacement_character()
{
return "\xEF\xBF\xBD";
}
} // namespace
TEST_CASE("regression test - #3989 SAX parse_error() returning true")
{
SECTION("binary formats complete what was read before the input ends")
{
const json j = {{"a", {1, -2, {{"b", "c"}}, json::array()}}, {"d", {{"e", nullptr}, {"f", true}}}, {"g", 1.5}, {"h", json::binary({1, 2, 3})}};
const std::vector<std::pair<json::input_format_t, std::vector<std::uint8_t>>> encodings =
{
{json::input_format_t::cbor, json::to_cbor(j)},
{json::input_format_t::msgpack, json::to_msgpack(j)},
{json::input_format_t::ubjson, json::to_ubjson(j)},
{json::input_format_t::ubjson, json::to_ubjson(j, true, true)},
{json::input_format_t::bjdata, json::to_bjdata(j)},
{json::input_format_t::bjdata, json::to_bjdata(j, true, true)},
{json::input_format_t::bson, json::to_bson(j)},
{json::input_format_t::bon8, json::to_bon8(j)},
};
for (const auto& encoding : encodings)
{
const auto format = encoding.first;
const auto& bytes = encoding.second;
CAPTURE(format)
// every prefix is truncated input
for (std::size_t length = 0; length < bytes.size(); ++length)
{
CAPTURE(length)
const auto result = parse_binary_recovering(std::vector<std::uint8_t>(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length)), format);
CHECK(!result.ok);
CHECK(result.errors == 1);
CHECK(result.balanced);
}
// the complete input is read as usual (binary values do not
// round-trip through every format, so compare with a plain parse)
json expected;
nlohmann::detail::json_sax_dom_parser<json> dom(expected);
CHECK(json::sax_parse(bytes, &dom, format));
const auto complete = parse_binary_recovering(bytes, format);
CHECK(complete.ok);
CHECK(complete.errors == 0);
CHECK(complete.value == expected);
// a byte after the value
auto trailing_bytes = bytes;
trailing_bytes.push_back(0x01);
const auto trailing = parse_binary_recovering(trailing_bytes, format);
CHECK(!trailing.ok);
CHECK(trailing.errors == 1);
CHECK(trailing.value == expected);
}
}
SECTION("containers without an end")
{
// these made the readers loop, or read on, after the error
const auto cbor_array = parse_binary_recovering({0x9F}, json::input_format_t::cbor);
CHECK(cbor_array.errors == 1);
CHECK(cbor_array.value == json::array());
const auto cbor_map = parse_binary_recovering({0xBF, 0x61, 'a'}, json::input_format_t::cbor);
CHECK(cbor_map.errors == 1);
CHECK(cbor_map.value == json({{"a", nullptr}}));
const auto msgpack_array = parse_binary_recovering({0xDD, 0xFF, 0xFF, 0xFF, 0xFF}, json::input_format_t::msgpack);
CHECK(msgpack_array.errors == 1);
CHECK(msgpack_array.value == json::array());
const auto msgpack_map = parse_binary_recovering({0x81, 0xA1, 'a', 0x92, 0x01}, json::input_format_t::msgpack);
CHECK(msgpack_map.errors == 1);
CHECK(msgpack_map.value == json({{"a", {1}}}));
}
SECTION("BJData ndarray")
{
// a 2x3 int8 array with two of its six elements; the annotated array
// format opens an object and two arrays of its own
const auto result = parse_binary_recovering({'[', '$', 'i', '#', '[', '$', 'i', '#', 'i', 2, 2, 3, 1, 2}, json::input_format_t::bjdata);
CHECK(result.errors == 1);
CHECK(result.balanced);
CHECK(result.value == json({{"_ArrayType_", "int8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2}}}));
}
SECTION("binary formats repair items whose end is known")
{
struct Repair
{
json::input_format_t format;
std::vector<std::uint8_t> input;
json expected;
std::size_t errors;
};
const std::vector<Repair> repairs =
{
// CBOR: tags are ignored (here tag 1 and the self-describe tag 55799)
{json::input_format_t::cbor, {0x82, 0xC1, 0x05, 0xD9, 0xD9, 0xF7, 0x06}, {5, 6}, 2},
// CBOR: undefined and other simple values become null
{json::input_format_t::cbor, {0x84, 0xF7, 0xE0, 0xF8, 0x20, 0x01}, {nullptr, nullptr, nullptr, 1}, 3},
// CBOR: members whose key is not a string are skipped, whatever their key and value
{json::input_format_t::cbor, {0xA4, 0x01, 0x02, 0x82, 0x01, 0x02, 0xA1, 0x61, 'x', 0x9F, 0xFF, 0xC1, 0x01, 0x5F, 0x41, 0x00, 0xFF, 0x61, 'a', 0x03}, {{"a", 3}}, 3},
{json::input_format_t::cbor, {0xBF, 0xF5, 0xBF, 0x61, 'x', 0x7F, 0x61, 'y', 0xFF, 0xFF, 0x61, 'a', 0x03, 0xFF}, {{"a", 3}}, 1},
// MessagePack: members whose key is not a string are skipped
{json::input_format_t::msgpack, {0x84, 0x01, 0x02, 0x81, 0xA1, 'x', 0x01, 0x92, 0x01, 0x02, 0xD4, 0x01, 0x02, 0xC0, 0xA1, 'a', 0x04}, {{"a", 4}}, 3},
// UBJSON: a char that is not ASCII becomes U+FFFD
{json::input_format_t::ubjson, {'[', 'C', 0x80, 'C', 'A', ']'}, {replacement_character(), "A"}, 1},
// UBJSON: the longest beginning of a high-precision number is kept
{json::input_format_t::ubjson, {'[', 'H', 'i', 5, '1', '2', 'a', 'b', 'c', 'H', 'i', 2, '1', '.', 'H', 'i', 3, 'a', 'b', 'c', 'H', 'i', 3, '4', '.', '5', ']'}, {12, 1, nullptr, 4.5}, 3},
// BJData, too
{json::input_format_t::bjdata, {'[', 'C', 0xFF, 'H', 'i', 2, '-', '1', 'H', 'i', 2, '-', 'x', ']'}, {replacement_character(), -1, nullptr}, 2},
// a NUL ends a high-precision number, as it ends JSON text
{json::input_format_t::ubjson, {'[', 'H', 'i', 4, '1', '2', 0, '9', 'H', 'i', 2, 0, '1', 'i', 3, ']'}, {12, nullptr, 3}, 2},
// BON8: members whose key is not a string are skipped
{json::input_format_t::bon8, {0x89, 0x91, 0x92, 0xC9, 0x40, 0x82, 0x91, 0x92, 0x61, 0x93}, {{"a", 3}}, 2},
{json::input_format_t::bon8, {0x8B, 0x91, 0x85, 0x91, 0xFE, 0xFA, 0x8B, 'x', 0x91, 0xFE, 0x61, 0x93, 0xFE}, {{"a", 3}}, 2},
// BSON: elements of types the library does not read become null
{
json::input_format_t::bson, bson_document(
{
bson_element(0x07, "_id", bytes(12)), // ObjectId
bson_element(0x09, "date", bytes(8)), // UTC datetime
bson_element(0x13, "decimal", bytes(16)), // 128-bit decimal
bson_element(0x0B, "regex", {'a', '+', 0, 'i', 0}), // regular expression
bson_element(0x0D, "code", bson_string("f()")), // JavaScript code
bson_element(0x0E, "symbol", bson_string("s")), // symbol
bson_element(0x0C, "pointer", concatenated({bson_string("c"), bytes(12)})), // DBPointer
bson_element(0x0F, "scope", concatenated({bson_int32(15), bson_string("g"), bson_document({})})), // code with scope
bson_element(0x06, "undefined", {}), // undefined
bson_element(0xFF, "min", {}), // min key
bson_element(0x7F, "max", {}), // max key
bson_element(0x10, "z", bson_int32(7)),
}),
{{"_id", nullptr}, {"date", nullptr}, {"decimal", nullptr}, {"regex", nullptr}, {"code", nullptr}, {"symbol", nullptr}, {"pointer", nullptr}, {"scope", nullptr}, {"undefined", nullptr}, {"min", nullptr}, {"max", nullptr}, {"z", 7}},
11
},
// BSON: an element of an unknown type becomes null, and the rest of its document is skipped
{
json::input_format_t::bson, bson_document(
{
bson_element(0x03, "inner", bson_document({bson_element(0x10, "a", bson_int32(1)), bson_element(0x42, "x", bytes(3)), bson_element(0x10, "b", bson_int32(2))})),
bson_element(0x04, "array", bson_document({bson_element(0x10, "0", bson_int32(1)), bson_element(0x42, "1", bytes(3))})),
bson_element(0x10, "after", bson_int32(3)),
}),
{{"inner", {{"a", 1}, {"x", nullptr}}}, {"array", {1, nullptr}}, {"after", 3}},
2
},
// BSON: so does a string or byte array whose length cannot be right
{
json::input_format_t::bson, bson_document(
{
bson_element(0x03, "inner", bson_document({bson_element(0x02, "s", bson_string("abc", -10)), bson_element(0x10, "b", bson_int32(2))})),
bson_element(0x03, "bin", bson_document({bson_element(0x05, "b", concatenated({bson_int32(-1), bytes(1)})), bson_element(0x10, "b", bson_int32(2))})),
bson_element(0x10, "after", bson_int32(3)),
}),
{{"inner", {{"s", nullptr}}}, {"bin", {{"b", nullptr}}}, {"after", 3}},
2
},
// BSON: a string without its terminator, and a document whose size does not match, are kept
{
json::input_format_t::bson, bson_document(
{
bson_element(0x02, "s", {2, 0, 0, 0, 'a', 'X'}),
bson_element(0x03, "inner", bson_document({bson_element(0x10, "a", bson_int32(1))}, 1)),
}),
{{"s", "a"}, {"inner", {{"a", 1}}}},
2
},
};
for (const auto& repair : repairs)
{
CAPTURE(repair.format)
CAPTURE(repair.input)
const auto result = parse_binary_recovering(repair.input, repair.format);
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.errors == repair.errors);
CHECK(result.value == repair.expected);
REQUIRE(!result.messages.empty());
#if !defined(JSON_NOEXCEPTION)
// the first error is the one reported without recovering; under
// JSON_NOEXCEPTION, reading without recovering aborts instead of
// throwing, so there is no message to compare with
CHECK(result.messages.front() == binary_error_message(repair.input, repair.format));
#endif
}
}
SECTION("binary formats repair numbers that are out of range")
{
// CBOR: a double too large for a float number_float_t
float_json cbor;
BasicRecoveringParser<float_json> sax(cbor);
const std::vector<std::uint8_t> cbor_input = {0x82, 0xFB, 0x7E, 0x37, 0xE4, 0x3C, 0x88, 0x00, 0x75, 0x9C, 0x01}; // [1e300, 1]
CHECK(!float_json::sax_parse(cbor_input, &sax, float_json::input_format_t::cbor));
CHECK(sax.errors == 1);
CHECK(sax.messages.front() == "[json.exception.out_of_range.406] syntax error while parsing CBOR value: number overflow");
REQUIRE(cbor.size() == 2);
CHECK(std::isinf(cbor[0].get<float>()));
CHECK(cbor[1] == 1);
// UBJSON: a high-precision number too large for number_float_t
const auto ubjson = parse_binary_recovering({'H', 'i', 5, '1', 'e', '9', '9', '9'}, json::input_format_t::ubjson);
CHECK(ubjson.errors == 1);
CHECK(ubjson.value.is_number_float());
CHECK(std::isinf(ubjson.value.get<double>()));
}
SECTION("binary formats stop where the end of an item is not known")
{
// a byte that begins no item
const auto cbor = parse_binary_recovering({0x82, 0x01, 0x1C, 0x02}, json::input_format_t::cbor);
CHECK(cbor.errors == 1);
CHECK(cbor.value == json({1}));
// a key that is no item: the unused MessagePack byte, a CBOR break
// in a map of known size, and the end of a BON8 container
const auto msgpack = parse_binary_recovering({0x82, 0xA1, 'a', 0x01, 0xC1, 0x02}, json::input_format_t::msgpack);
CHECK(msgpack.errors == 1);
CHECK(msgpack.value == json({{"a", 1}}));
const auto cbor_break = parse_binary_recovering({0xA2, 0x61, 'a', 0x01, 0xFF, 0x02}, json::input_format_t::cbor);
CHECK(cbor_break.errors == 1);
CHECK(cbor_break.value == json({{"a", 1}}));
const auto bon8 = parse_binary_recovering({0x88, 0x61, 0x91, 0xFE}, json::input_format_t::bon8);
CHECK(bon8.errors == 1);
CHECK(bon8.value == json({{"a", 1}}));
// an indefinite-length string inside an indefinite-length string
const auto nested = parse_binary_recovering({0x82, 0x01, 0x7F, 0x7F, 0x61, 'a', 0xFF, 0xFF}, json::input_format_t::cbor);
CHECK(nested.errors == 1);
CHECK(nested.value == json({1}));
// a BJData ndarray whose element type has no name: the object that
// holds the ndarray was already begun
const auto ndarray = parse_binary_recovering({'[', '$', 0x01, '#', '[', '$', 'i', '#', 'i', 2, 2, 3}, json::input_format_t::bjdata);
CHECK(ndarray.errors == 1);
CHECK(ndarray.balanced);
CHECK(ndarray.value == json::object());
// a skipped member that the input ends in
const auto truncated = parse_binary_recovering({0xA2, 0x01, 0x82, 0x01}, json::input_format_t::cbor);
CHECK(truncated.errors == 2);
CHECK(truncated.balanced);
CHECK(truncated.value == json::object());
// a BSON element of an unknown type in a document whose size cannot be right
const auto bson = parse_binary_recovering(bson_document({bson_element(0x10, "a", bson_int32(1)), bson_element(0x42, "x", bytes(3))}, -10), json::input_format_t::bson);
CHECK(bson.errors == 1);
CHECK(bson.value == json({{"a", 1}, {"x", nullptr}}));
}
SECTION("changed bytes in binary input")
{
const json j = {{"a", {1, -2, {{"b", "c"}}, json::array()}}, {"d", {{"e", nullptr}, {"f", true}}}, {"g", 1.5}, {"h", json::binary({1, 2, 3})}, {"i", "\xC3\xA4"}};
const std::vector<std::pair<json::input_format_t, std::vector<std::uint8_t>>> encodings =
{
{json::input_format_t::cbor, json::to_cbor(j)},
{json::input_format_t::msgpack, json::to_msgpack(j)},
{json::input_format_t::ubjson, json::to_ubjson(j)},
{json::input_format_t::ubjson, json::to_ubjson(j, true, true)},
{json::input_format_t::bjdata, json::to_bjdata(j)},
{json::input_format_t::bjdata, json::to_bjdata(j, true, true)},
{json::input_format_t::bson, json::to_bson(j)},
{json::input_format_t::bon8, json::to_bon8(j)},
};
const std::vector<std::uint8_t> replacements = {0x00, 0x01, 0x7F, 0x80, 0xC1, 0xD9, 0xE0, 0xF7, 0xFE, 0xFF};
for (const auto& encoding : encodings)
{
const auto format = encoding.first;
const auto& original = encoding.second;
CAPTURE(format)
std::vector<std::vector<std::uint8_t>> inputs;
for (std::size_t position = 0; position < original.size(); ++position)
{
for (const auto replacement : replacements)
{
auto changed = original;
changed[position] = replacement;
inputs.push_back(changed);
}
auto removed = original;
removed.erase(removed.begin() + static_cast<std::ptrdiff_t>(position));
inputs.push_back(removed);
}
for (const auto& input : inputs)
{
CAPTURE(input)
const auto result = parse_binary_recovering(input, format);
CHECK(result.balanced);
CHECK(result.errors <= input.size() + 1);
#if !defined(JSON_NOEXCEPTION)
// an error is reported exactly if reading into a JSON value
// fails, and the first one is the same (under JSON_NOEXCEPTION,
// that reading aborts instead of throwing)
const auto message = binary_error_message(input, format);
CHECK(result.ok == message.empty());
if (!result.ok && result.errors < 100)
{
CHECK(result.messages.front() == message);
}
#endif
}
}
}
SECTION("JSON text")
{
// the parser stopped, but reported success
json j;
RecoveringParser sax(j);
CHECK(!json::sax_parse("[1,2,3,]", &sax));
CHECK(sax.errors == 1);
CHECK(j == json({1, 2, 3}));
}
SECTION("the SAX parsers of the library stop")
{
json _;
CHECK(json::from_cbor(std::vector<std::uint8_t> {0x9F}, true, false).is_discarded());
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t> {0x9F}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&);
CHECK(json::parse("[1,2,3,]", nullptr, false).is_discarded());
CHECK(!json::accept("[1,2,3,]"));
}
}
TEST_CASE("issue #5676 - SAX parsing of CBOR tags")
{
const json expected = json::binary({1, 2, 3}, 42);
const auto cbor = json::to_cbor(expected);
nlohmann::detail::json_sax_acceptor<json> acceptor;
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor));
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::error));
CHECK(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::ignore));
json parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(parsed);
CHECK(json::sax_parse(cbor, &sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(parsed == expected);
json iterator_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> iterator_sax(iterator_parsed);
CHECK(json::sax_parse(cbor.begin(), cbor.end(), &iterator_sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(iterator_parsed == expected);
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
json span_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> span_sax(span_parsed);
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(cbor.data(), cbor.size()), &span_sax,
json::input_format_t::cbor, true, false, false, json::cbor_tag_handler_t::store));
CHECK(span_parsed == expected);
#endif
const std::string text = "null";
CHECK(json::sax_parse(text, &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(json::sax_parse(text.begin(), text.end(), &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(text.data(), text.size()), &acceptor,
json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store));
#endif
}
TEST_CASE("MessagePack SAX aborts")
{
SECTION("start_array(len)")
{
std::vector<uint8_t> const v = {0x93, 0x01, 0x02, 0x03};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("start_object(len)")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("key()")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(1);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
}
TEST_CASE("issue #5405 - a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_msgpack(j);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::msgpack));
}
TEST_CASE("MessagePack nesting does not consume the call stack - SAX interface")
{
// see the test case of the same name in unit-msgpack.cpp (#5104)
std::vector<uint8_t> input(300000, 0x91);
input.push_back(0x01); // innermost value
SaxCountdown accept_all(600001);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::msgpack));
}
TEST_CASE("MessagePack SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::msgpack))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_msgpack(j)) == 20);
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP