mirror of
https://github.com/nlohmann/json.git
synced 2026-10-18 12:08:12 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f25c6543aa | ||
|
|
c7a9780936 | ||
|
|
1d2526b1d8 |
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
|
|||||||
- [operator>>](../operator_gtgt.md) - deserialize from stream
|
- [operator>>](../operator_gtgt.md) - deserialize from stream
|
||||||
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
|
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
|
||||||
instead of treating it as end of input
|
instead of treating it as end of input
|
||||||
|
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
|
||||||
|
at a time
|
||||||
|
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
|
||||||
|
functions
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
@@ -143,6 +147,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
|
|||||||
- 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_STRICT_NUL_HANDLING` added in version 3.13.0 to optionally reject a NUL byte in the input instead of treating
|
- `JSON_STRICT_NUL_HANDLING` added in version 3.13.0 to optionally reject a NUL byte in the input instead of treating
|
||||||
it as end of input; planned to become the default in version 4.0.0.
|
it as end of input; planned to become the default in version 4.0.0.
|
||||||
|
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
|
||||||
|
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
|
||||||
|
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
|
||||||
|
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
|
||||||
|
|
||||||
!!! warning "Deprecation"
|
!!! warning "Deprecation"
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,16 @@ By default, a `'\0'` (NUL) byte anywhere in the input is treated as end of input
|
|||||||
outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-bytes-in-the-input) for details and the
|
outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-bytes-in-the-input) for details and the
|
||||||
[`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) macro to opt into rejecting it instead.
|
[`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) macro to opt into rejecting it instead.
|
||||||
|
|
||||||
|
!!! tip "Performance"
|
||||||
|
|
||||||
|
Contiguous inputs (strings, string views, vectors, arrays, pointer ranges) are the fastest to parse. A
|
||||||
|
`#!cpp std::istream` comes close: the parser scans the characters its stream buffer holds in bulk, without reading
|
||||||
|
ahead, so the stream is left exactly where it would be after reading one character at a time. A `#!cpp FILE*` is
|
||||||
|
read one character at a time, but the file is locked only once, so other threads that use the same `FILE` object
|
||||||
|
wait until parsing is done. See [input sources and performance](../../features/parsing/index.md#input-sources-and-performance),
|
||||||
|
[`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md), and
|
||||||
|
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
??? example "Example: (1) parse from a character array"
|
??? example "Example: (1) parse from a character array"
|
||||||
@@ -247,6 +257,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
|
|||||||
- [operator>>](../operator_gtgt.md) - deserialize from stream
|
- [operator>>](../operator_gtgt.md) - deserialize from stream
|
||||||
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
|
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
|
||||||
instead of treating it as end of input
|
instead of treating it as end of input
|
||||||
|
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
|
||||||
|
at a time
|
||||||
|
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
|
||||||
|
functions
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
@@ -261,6 +275,11 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
|
|||||||
it as end of input; planned to become the default in version 4.0.0.
|
it as end of input; planned to become the default in version 4.0.0.
|
||||||
- Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash
|
- Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash
|
||||||
(`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0.
|
(`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0.
|
||||||
|
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
|
||||||
|
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) restores reading one character at a
|
||||||
|
time).
|
||||||
|
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
|
||||||
|
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) restores `std::fgetc`).
|
||||||
|
|
||||||
!!! warning "Deprecation"
|
!!! warning "Deprecation"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -118,6 +116,10 @@ consumer `sax` has a super-linear complexity.
|
|||||||
|
|
||||||
A UTF-8 byte order mark is silently ignored.
|
A UTF-8 byte order mark is silently ignored.
|
||||||
|
|
||||||
|
When reading from a `#!c FILE*`, the file is locked while it is parsed. The lock is recursive, so the SAX handler may
|
||||||
|
use the same file with stdio functions, but it must not wait for another thread that uses the same `FILE` object; see
|
||||||
|
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
??? example
|
??? example
|
||||||
@@ -139,6 +141,10 @@ A UTF-8 byte order mark is silently ignored.
|
|||||||
|
|
||||||
- [parse](parse.md) - deserialize from a compatible input
|
- [parse](parse.md) - deserialize from a compatible input
|
||||||
- [accept](accept.md) - check if the input is valid JSON
|
- [accept](accept.md) - check if the input is valid JSON
|
||||||
|
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
|
||||||
|
at a time
|
||||||
|
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
|
||||||
|
functions
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
@@ -147,10 +153,13 @@ 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`.
|
||||||
|
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
|
||||||
|
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
|
||||||
|
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
|
||||||
|
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
|
||||||
|
|
||||||
!!! warning "Deprecation"
|
!!! warning "Deprecation"
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -16,6 +16,10 @@ header. See also the [macro overview page](../../features/macros.md).
|
|||||||
|
|
||||||
## Parsing
|
## Parsing
|
||||||
|
|
||||||
|
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
|
||||||
|
instead of scanning its stream buffer in bulk
|
||||||
|
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with `std::fgetc` instead of
|
||||||
|
locking it once and using the unlocked stdio functions
|
||||||
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - opt in to leaving an input stream positioned
|
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - opt in to leaving an input stream positioned
|
||||||
right after a parsed number
|
right after a parsed number
|
||||||
- [**JSON_STRICT_BINARY_UTF8**](json_strict_binary_utf8.md) - opt in to checking strings for valid UTF-8 in the CBOR,
|
- [**JSON_STRICT_BINARY_UTF8**](json_strict_binary_utf8.md) - opt in to checking strings for valid UTF-8 in the CBOR,
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define JSON_DISABLE_STREAM_BULK_SCAN /* value */
|
||||||
|
```
|
||||||
|
|
||||||
|
When defined to `1`, parsing from a `#!cpp std::istream` reads the input one character at a time through the stream
|
||||||
|
buffer, as in version 3.12.0 and earlier, instead of scanning the characters that the stream buffer already holds in
|
||||||
|
bulk.
|
||||||
|
|
||||||
|
The macro is an escape hatch for stream buffers that do not follow the usual contract of `std::basic_streambuf` (see
|
||||||
|
the requirements on the stream buffer in the notes below). It does not change any result: values, exceptions, and the
|
||||||
|
position of the stream after parsing are the same either way. Only the speed differs.
|
||||||
|
|
||||||
|
## Default definition
|
||||||
|
|
||||||
|
The default value is `0`: streams are scanned in bulk.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define JSON_DISABLE_STREAM_BULK_SCAN 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
!!! note "How streams are scanned"
|
||||||
|
|
||||||
|
A `std::streambuf` keeps the part of the input it has read but not yet handed out in its *get area*, the range
|
||||||
|
from `gptr()` to `egptr()`. A `std::filebuf`, for instance, reads the file in blocks of a few kilobytes into its get
|
||||||
|
area, and a `std::stringbuf` exposes its whole string as one get area.
|
||||||
|
|
||||||
|
When parsing from a `std::istream`, the parser scans this get area directly, with the same fast paths that it uses
|
||||||
|
for a `std::string`: runs of ordinary string characters, numbers, and indentation are recognized several bytes at a
|
||||||
|
time instead of being requested from the stream buffer one character at a time. When the parser reaches the end of
|
||||||
|
the get area, it asks the stream buffer for the next character as before, which makes the stream buffer refill its
|
||||||
|
get area.
|
||||||
|
|
||||||
|
The parser only reads characters that it would also have read one at a time, and it advances the stream buffer
|
||||||
|
exactly over the characters it consumes. Nothing is read ahead, buffered elsewhere, or put back. Therefore, the
|
||||||
|
stream is left exactly where it was left before:
|
||||||
|
|
||||||
|
- [`operator>>`](../operator_gtgt.md) and [`sax_parse`](../basic_json/sax_parse.md) with `strict = false` can still
|
||||||
|
read several values from one stream, and the stream can still be used with other `std::istream` functions after a
|
||||||
|
value has been read.
|
||||||
|
- With [`JSON_PRECISE_STREAM_POSITION`](json_precise_stream_position.md), the stream is still positioned right after
|
||||||
|
a parsed number.
|
||||||
|
- Exception messages, including the line, the column, and the last read token, are unchanged.
|
||||||
|
|
||||||
|
!!! note "Performance"
|
||||||
|
|
||||||
|
Parsing from a `std::ifstream` or `std::istringstream` is typically 1.5 to 2 times as fast as before, and reading
|
||||||
|
many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream is then within
|
||||||
|
about 10 to 20 % of first reading the whole file into a `std::string` and parsing that.
|
||||||
|
|
||||||
|
A stream buffer without a get area (one that only overrides `underflow()` and `uflow()` and hands out every
|
||||||
|
character from there) offers nothing to scan in bulk, so it is read one character at a time as before.
|
||||||
|
|
||||||
|
!!! note "Requirements on the stream buffer"
|
||||||
|
|
||||||
|
The bulk scan relies on the following behavior, which every stream buffer of the standard library and every stream
|
||||||
|
buffer that keeps its input in its get area has:
|
||||||
|
|
||||||
|
- When `sgetc()` returns a character while `gptr() < egptr()`, that character is `*gptr()`; when `sbumpc()` returns
|
||||||
|
one, it was at `gptr()` before the call.
|
||||||
|
- Characters in the get area are not changed while they are in the get area.
|
||||||
|
- `underflow()` or `uflow()` either sets up a get area that holds the returned character at `gptr()` (or at
|
||||||
|
`gptr() - 1` after `uflow()`), or leaves the get area empty.
|
||||||
|
|
||||||
|
A stream buffer that returns characters from `uflow()` while leaving unrelated characters in its get area, or that
|
||||||
|
changes the characters in its get area in place, does not meet these requirements. Define the macro to `1` for
|
||||||
|
such stream buffers.
|
||||||
|
|
||||||
|
!!! note "ABI compatibility"
|
||||||
|
|
||||||
|
The value of this macro is encoded in the [namespace](../../features/namespace.md) (tag `_dsbs`), resulting in
|
||||||
|
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
|
||||||
|
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
|
||||||
|
|
||||||
|
!!! note "Other inputs"
|
||||||
|
|
||||||
|
Strings, string views, vectors, arrays, and pointer or iterator ranges over contiguous memory are scanned in bulk
|
||||||
|
anyway. Reading from a `#!cpp FILE*` is not affected by this macro; see
|
||||||
|
[`JSON_DISABLE_UNLOCKED_FILE_IO`](json_disable_unlocked_file_io.md) for that input.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
??? example "Example: reading values from a stream (default)"
|
||||||
|
|
||||||
|
The stream is scanned in bulk, and the values are read one after the other:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
|
||||||
|
json j1, j2, j3;
|
||||||
|
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
|
||||||
|
|
||||||
|
std::string word;
|
||||||
|
input >> word; // "rest"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
??? example "Example: reading one character at a time (macro defined to 1)"
|
||||||
|
|
||||||
|
With the macro, the same program reads the stream one character at a time, with the same results:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define JSON_DISABLE_STREAM_BULK_SCAN 1
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
|
||||||
|
json j1, j2, j3;
|
||||||
|
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
|
||||||
|
|
||||||
|
std::string word;
|
||||||
|
input >> word; // "rest"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
|
||||||
|
- [**operator>>**](../operator_gtgt.md) - deserialize from stream
|
||||||
|
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
|
||||||
|
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - leave an input stream positioned right after a
|
||||||
|
parsed number
|
||||||
|
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
|
||||||
|
functions
|
||||||
|
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
- Added in version 3.13.0.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define JSON_DISABLE_UNLOCKED_FILE_IO /* value */
|
||||||
|
```
|
||||||
|
|
||||||
|
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, as in version 3.12.0 and
|
||||||
|
earlier, instead of locking the file once and reading it with the unlocked stdio functions.
|
||||||
|
|
||||||
|
It does not change any result: values, exceptions, and the position of the file after parsing are the same either way.
|
||||||
|
Only the speed and the locking of the file differ (see the notes on locking below).
|
||||||
|
|
||||||
|
## Default definition
|
||||||
|
|
||||||
|
The default value is `0`: the file is locked once and read with the unlocked stdio functions where they are available.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
!!! note "Background"
|
||||||
|
|
||||||
|
The C standard library makes every `std::fgetc` call thread-safe by locking and unlocking the `FILE` object. When
|
||||||
|
parsing from a `FILE*`, the parser reads one character at a time, so this happens for every character of the input.
|
||||||
|
|
||||||
|
Instead, the parser locks the file once when parsing starts and unlocks it when parsing ends - also when an
|
||||||
|
exception is thrown - and reads the characters in between without locking:
|
||||||
|
|
||||||
|
| C library | functions |
|
||||||
|
|------------------------------------------------------------|----------------------------------------------------|
|
||||||
|
| glibc (Linux), Apple (macOS, iOS), Android, FreeBSD, NetBSD, OpenBSD, DragonFly BSD | `flockfile`, `getc_unlocked`, `funlockfile` (POSIX) |
|
||||||
|
| Microsoft (MSVC, clang-cl) | `_lock_file`, `_fgetc_nolock`, `_unlock_file` |
|
||||||
|
| others (e.g., musl, MinGW, newlib) | `std::fgetc`, as before |
|
||||||
|
|
||||||
|
This makes parsing from a `FILE*` 3 to 4 times as fast with the Apple C library and 1.1 to 1.3 times as fast with
|
||||||
|
glibc, whose locking is cheaper. The binary formats ([`from_cbor`](../basic_json/from_cbor.md),
|
||||||
|
[`from_msgpack`](../basic_json/from_msgpack.md), ...) read from a `FILE*` the same way.
|
||||||
|
|
||||||
|
!!! note "Locking"
|
||||||
|
|
||||||
|
While a value is parsed from a `FILE*`, the parsing thread holds the file's lock. The lock is recursive, so the
|
||||||
|
parsing thread itself - for instance, a [SAX](../json_sax/index.md) handler or a
|
||||||
|
[parser callback](../basic_json/parser_callback_t.md) - can keep using the file with stdio functions such as
|
||||||
|
`std::ftell`. Other threads that use the same `FILE` object wait until the parser is done. Before, they could
|
||||||
|
interleave their reads with the parser's, which made the parsed input unpredictable anyway.
|
||||||
|
|
||||||
|
A program in which a SAX handler or parser callback waits for **another** thread that uses the **same** `FILE`
|
||||||
|
object would deadlock. Define the macro to `1` for such programs.
|
||||||
|
|
||||||
|
!!! note "ABI compatibility"
|
||||||
|
|
||||||
|
The value of this macro is encoded in the [namespace](../../features/namespace.md) (tag `_dufio`), resulting in
|
||||||
|
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
|
||||||
|
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
|
||||||
|
|
||||||
|
!!! note "Other inputs"
|
||||||
|
|
||||||
|
Reading from a `#!cpp std::istream` is not affected by this macro; see
|
||||||
|
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md) for that input.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
??? example "Example: parsing from a `FILE*` (default)"
|
||||||
|
|
||||||
|
The file is locked once while it is parsed:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cstdio>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
std::FILE* f = std::fopen("example.json", "rb");
|
||||||
|
json j = json::parse(f); // f is locked until parse() returns
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
??? example "Example: locking for every character (macro defined to 1)"
|
||||||
|
|
||||||
|
With the macro, every character is read with `std::fgetc`, with the same result:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define JSON_DISABLE_UNLOCKED_FILE_IO 1
|
||||||
|
#include <cstdio>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
std::FILE* f = std::fopen("example.json", "rb");
|
||||||
|
json j = json::parse(f); // std::fgetc for every character
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
|
||||||
|
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
|
||||||
|
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a
|
||||||
|
time
|
||||||
|
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
- Added in version 3.13.0.
|
||||||
@@ -72,6 +72,12 @@ The default value is `0` (disabled — existing behavior is preserved).
|
|||||||
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
|
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
|
||||||
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
|
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
|
||||||
|
|
||||||
|
!!! note "Bulk scanning"
|
||||||
|
|
||||||
|
The parser scans the characters in the stream buffer in bulk (see
|
||||||
|
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)), but it never reads ahead. The stream is left
|
||||||
|
at the same position with and without bulk scanning, in both modes of this macro.
|
||||||
|
|
||||||
!!! tip "Workaround without the macro"
|
!!! tip "Workaround without the macro"
|
||||||
|
|
||||||
Separate the values in the stream with whitespace. The character consumed after a number is then the separator,
|
Separate the values in the stream with whitespace. The character consumed after a number is then the separator,
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ non-ASCII characters (e.g. CJK or emoji) this can validate several times faster.
|
|||||||
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
|
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
|
||||||
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
|
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
|
||||||
message (simdutf is only used to fast-path *valid* runs; anything it flags falls back to the scalar path so the exact
|
message (simdutf is only used to fast-path *valid* runs; anything it flags falls back to the scalar path so the exact
|
||||||
diagnostic is preserved). Streaming inputs (files, `std::istream`, wide strings, user-defined adapters) always use the
|
diagnostic is preserved). A `std::istream` uses simdutf for the characters its stream buffer holds (see
|
||||||
scalar path.
|
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other streaming inputs (`FILE*`, wide strings,
|
||||||
|
user-defined adapters) always use the scalar path.
|
||||||
|
|
||||||
When `JSON_USE_SIMDUTF` is defined you must make the `simdutf.h` header available on the include path and link the
|
When `JSON_USE_SIMDUTF` is defined you must make the `simdutf.h` header available on the include path and link the
|
||||||
simdutf library. When it is not defined, no simdutf header is included and there is no dependency.
|
simdutf library. When it is not defined, no simdutf header is included and there is no dependency.
|
||||||
|
|||||||
@@ -39,5 +39,6 @@ the library.
|
|||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
- Added in version 3.11.0. Changed inline namespace name in version 3.11.2. Added the ABI tag `_dp` in
|
- Added in version 3.11.0. Changed inline namespace name in version 3.11.2. Added the ABI tag `_dp` in
|
||||||
version 3.12.0, and the ABI tags `_bics`, `_psp`, `_snul`, `_sbu8`, and `_ekmo` in version 3.13.0; see
|
version 3.12.0, and the ABI tags `_bics`, `_psp`, `_snul`, `_sbu8`, `_ekmo`, `_dsbs`, and `_dufio` in version
|
||||||
|
3.13.0; see
|
||||||
[`nlohmann` Namespace](../../features/namespace.md#structure).
|
[`nlohmann` Namespace](../../features/namespace.md#structure).
|
||||||
@@ -121,6 +121,7 @@ being read.
|
|||||||
- [`JSON_STRICT_NUL_HANDLING`](macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
|
- [`JSON_STRICT_NUL_HANDLING`](macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
|
||||||
instead of treating it as end of input
|
instead of treating it as end of input
|
||||||
- [`JSON_PRECISE_STREAM_POSITION`](macros/json_precise_stream_position.md) - opt in to leaving the stream positioned right after a number
|
- [`JSON_PRECISE_STREAM_POSITION`](macros/json_precise_stream_position.md) - opt in to leaving the stream positioned right after a number
|
||||||
|
- [`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) - read the stream one character at a time
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
@@ -133,3 +134,6 @@ being read.
|
|||||||
crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.0.
|
crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.0.
|
||||||
- Changed to the strong exception safety guarantee in version 3.13.0: `j` is no longer left with a partially parsed
|
- Changed to the strong exception safety guarantee in version 3.13.0: `j` is no longer left with a partially parsed
|
||||||
value if parsing throws.
|
value if parsing throws.
|
||||||
|
- Scans the characters in the stream buffer of `i` in bulk in version 3.13.0; the position of `i` after the call is
|
||||||
|
unchanged ([`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) restores reading one character
|
||||||
|
at a time).
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -133,6 +133,14 @@ When defined, default parse and serialize functions for enums are excluded and h
|
|||||||
|
|
||||||
See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md).
|
See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md).
|
||||||
|
|
||||||
|
## `JSON_DISABLE_STREAM_BULK_SCAN`
|
||||||
|
|
||||||
|
When defined to `1`, parsing from a `#!cpp std::istream` reads one character at a time instead of scanning the
|
||||||
|
characters that the stream buffer holds in bulk. Results are the same either way; the macro is an escape hatch for
|
||||||
|
stream buffers that do not follow the usual `std::streambuf` contract. The default value is `0`.
|
||||||
|
|
||||||
|
See [full documentation of `JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md).
|
||||||
|
|
||||||
## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`
|
## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`
|
||||||
|
|
||||||
When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON
|
When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON
|
||||||
@@ -141,6 +149,14 @@ is planned to become the default in version 4.0.0.
|
|||||||
|
|
||||||
See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md).
|
See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md).
|
||||||
|
|
||||||
|
## `JSON_DISABLE_UNLOCKED_FILE_IO`
|
||||||
|
|
||||||
|
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, which locks the file each
|
||||||
|
time, instead of locking the file once and reading it with the unlocked stdio functions. Results are the same either
|
||||||
|
way. The default value is `0`.
|
||||||
|
|
||||||
|
See [full documentation of `JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md).
|
||||||
|
|
||||||
## `JSON_NO_AUTOMATIC_UDLS`
|
## `JSON_NO_AUTOMATIC_UDLS`
|
||||||
|
|
||||||
When defined, `<nlohmann/json.hpp>` does not include `<nlohmann/json_literals.hpp>` with the user-defined string literals
|
When defined, `<nlohmann/json.hpp>` does not include `<nlohmann/json_literals.hpp>` with the user-defined string literals
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ The complete default namespace name is derived as follows:
|
|||||||
- [`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md) defined non-zero appends `_sbu8`.
|
- [`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md) defined non-zero appends `_sbu8`.
|
||||||
- [`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md) defined non-zero
|
- [`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md) defined non-zero
|
||||||
appends `_ekmo`.
|
appends `_ekmo`.
|
||||||
|
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md) defined non-zero appends
|
||||||
|
`_dsbs`.
|
||||||
|
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) defined non-zero appends
|
||||||
|
`_dufio`.
|
||||||
- The inline namespace ends with the suffix `_v` followed by the 3 components of the version number separated by
|
- The inline namespace ends with the suffix `_v` followed by the 3 components of the version number separated by
|
||||||
underscores. To omit the version component, see [Disabling the version component](#disabling-the-version-component)
|
underscores. To omit the version component, see [Disabling the version component](#disabling-the-version-component)
|
||||||
below.
|
below.
|
||||||
@@ -105,5 +109,7 @@ follows:
|
|||||||
- Added ABI tags `_bics` ([`JSON_BRACE_INIT_COPY_SEMANTICS`](../api/macros/json_brace_init_copy_semantics.md)), `_psp`
|
- Added ABI tags `_bics` ([`JSON_BRACE_INIT_COPY_SEMANTICS`](../api/macros/json_brace_init_copy_semantics.md)), `_psp`
|
||||||
([`JSON_PRECISE_STREAM_POSITION`](../api/macros/json_precise_stream_position.md)), `_snul`
|
([`JSON_PRECISE_STREAM_POSITION`](../api/macros/json_precise_stream_position.md)), `_snul`
|
||||||
([`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md)), `_sbu8`
|
([`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md)), `_sbu8`
|
||||||
([`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md)), and `_ekmo`
|
([`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md)), `_ekmo`
|
||||||
([`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md)) in version 3.13.0.
|
([`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md)), `_dsbs`
|
||||||
|
([`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md)), and `_dufio`
|
||||||
|
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md)) in version 3.13.0.
|
||||||
@@ -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
|
|
||||||
@@ -55,6 +55,30 @@ whitespace, because `operator>>` consumes the character that terminates a number
|
|||||||
[`JSON_PRECISE_STREAM_POSITION`](../../api/macros/json_precise_stream_position.md) is defined to `1` — see the
|
[`JSON_PRECISE_STREAM_POSITION`](../../api/macros/json_precise_stream_position.md) is defined to `1` — see the
|
||||||
[`operator>>` notes](../../api/operator_gtgt.md#notes) for details and examples.
|
[`operator>>` notes](../../api/operator_gtgt.md#notes) for details and examples.
|
||||||
|
|
||||||
|
## Input sources and performance
|
||||||
|
|
||||||
|
All inputs produce the same values and the same exceptions, but they are read in different ways, which makes some of
|
||||||
|
them faster than others:
|
||||||
|
|
||||||
|
| input | how it is read |
|
||||||
|
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
|
||||||
|
| strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time |
|
||||||
|
| `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up |
|
||||||
|
| `#!cpp FILE*` | one character at a time, with the file locked once for the whole parse |
|
||||||
|
| other iterator ranges (e.g., `#!cpp std::list<char>`, `#!cpp std::istreambuf_iterator<char>`), wide strings | one character at a time |
|
||||||
|
|
||||||
|
Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a
|
||||||
|
`#!cpp std::string` first and parsing that, and it needs no memory for a copy of the file. The stream is never read
|
||||||
|
ahead: after parsing, it is positioned exactly as if every character had been read one at a time, so
|
||||||
|
[`operator>>`](../../api/operator_gtgt.md) can still read several values from one stream (see also
|
||||||
|
[JSON Lines](json_lines.md)).
|
||||||
|
|
||||||
|
Two macros switch these optimizations off without changing any result:
|
||||||
|
[`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not
|
||||||
|
follow the usual `std::streambuf` contract, and
|
||||||
|
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another
|
||||||
|
thread must use the same `#!cpp FILE` object while it is parsed.
|
||||||
|
|
||||||
## SAX vs. DOM parsing
|
## SAX vs. DOM parsing
|
||||||
|
|
||||||
The library offers two parsing models:
|
The library offers two parsing models:
|
||||||
@@ -75,7 +99,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 +110,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -298,7 +297,9 @@ nav:
|
|||||||
- 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md
|
- 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md
|
||||||
- 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.md
|
- 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.md
|
||||||
- 'JSON_DISABLE_ENUM_SERIALIZATION': api/macros/json_disable_enum_serialization.md
|
- 'JSON_DISABLE_ENUM_SERIALIZATION': api/macros/json_disable_enum_serialization.md
|
||||||
|
- 'JSON_DISABLE_STREAM_BULK_SCAN': api/macros/json_disable_stream_bulk_scan.md
|
||||||
- 'JSON_DISABLE_TUPLE_REFERENCE_CONVERSION': api/macros/json_disable_tuple_reference_conversion.md
|
- 'JSON_DISABLE_TUPLE_REFERENCE_CONVERSION': api/macros/json_disable_tuple_reference_conversion.md
|
||||||
|
- 'JSON_DISABLE_UNLOCKED_FILE_IO': api/macros/json_disable_unlocked_file_io.md
|
||||||
- 'JSON_HAS_CPP_11, JSON_HAS_CPP_14, JSON_HAS_CPP_17, JSON_HAS_CPP_20, JSON_HAS_CPP_23, JSON_HAS_CPP_26': api/macros/json_has_cpp_11.md
|
- 'JSON_HAS_CPP_11, JSON_HAS_CPP_14, JSON_HAS_CPP_17, JSON_HAS_CPP_20, JSON_HAS_CPP_23, JSON_HAS_CPP_26': api/macros/json_has_cpp_11.md
|
||||||
- 'JSON_HAS_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md
|
- 'JSON_HAS_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md
|
||||||
- 'JSON_HAS_RANGE_VIEW_CONVERSION': api/macros/json_has_range_view_conversion.md
|
- 'JSON_HAS_RANGE_VIEW_CONVERSION': api/macros/json_has_range_view_conversion.md
|
||||||
|
|||||||
@@ -54,6 +54,14 @@
|
|||||||
#define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0
|
#define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#define JSON_DISABLE_STREAM_BULK_SCAN 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
|
||||||
|
#endif
|
||||||
|
|
||||||
#if JSON_DIAGNOSTICS
|
#if JSON_DIAGNOSTICS
|
||||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
||||||
#else
|
#else
|
||||||
@@ -102,14 +110,26 @@
|
|||||||
#define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS
|
#define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN _dsbs
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO _dufio
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||||
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Construct the namespace ABI tags component
|
// Construct the namespace ABI tags component
|
||||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) json_abi ## a ## b ## c ## d ## e ## f ## g ## h
|
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j
|
||||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h) \
|
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j) \
|
||||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h)
|
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j)
|
||||||
|
|
||||||
#define NLOHMANN_JSON_ABI_TAGS \
|
#define NLOHMANN_JSON_ABI_TAGS \
|
||||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||||
@@ -120,7 +140,9 @@
|
|||||||
NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \
|
NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \
|
||||||
NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \
|
NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \
|
||||||
NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \
|
NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \
|
||||||
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS)
|
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO)
|
||||||
|
|
||||||
// Construct the namespace version component
|
// Construct the namespace version component
|
||||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -14,10 +14,12 @@
|
|||||||
#include <cstdint> // uint32_t
|
#include <cstdint> // uint32_t
|
||||||
#include <cstring> // strlen
|
#include <cstring> // strlen
|
||||||
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
|
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
|
||||||
|
#include <limits> // numeric_limits
|
||||||
#include <streambuf> // streambuf
|
#include <streambuf> // streambuf
|
||||||
#include <string> // string, char_traits
|
#include <string> // string, char_traits
|
||||||
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
|
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
|
||||||
#include <utility> // pair, declval
|
#include <utility> // pair, declval
|
||||||
|
#include <vector> // vector
|
||||||
|
|
||||||
#ifndef JSON_NO_IO
|
#ifndef JSON_NO_IO
|
||||||
#include <cstdio> // FILE *
|
#include <cstdio> // FILE *
|
||||||
@@ -30,6 +32,24 @@
|
|||||||
#include <nlohmann/detail/meta/type_traits.hpp>
|
#include <nlohmann/detail/meta/type_traits.hpp>
|
||||||
#include <nlohmann/detail/string_utils.hpp>
|
#include <nlohmann/detail/string_utils.hpp>
|
||||||
|
|
||||||
|
#ifndef JSON_NO_IO
|
||||||
|
// Which unlocked stdio functions file_input_adapter uses (see
|
||||||
|
// JSON_DISABLE_UNLOCKED_FILE_IO): 1 = POSIX flockfile/getc_unlocked,
|
||||||
|
// 2 = Microsoft CRT _lock_file/_fgetc_nolock, 0 = none (std::fgetc, which
|
||||||
|
// locks the file for every character). Only C libraries known to declare
|
||||||
|
// the functions are listed; the macros are tested after <cstdio>, which
|
||||||
|
// defines __GLIBC__.
|
||||||
|
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#define JSON_UNLOCKED_FILE_IO 0
|
||||||
|
#elif defined(_MSC_VER)
|
||||||
|
#define JSON_UNLOCKED_FILE_IO 2
|
||||||
|
#elif defined(__GLIBC__) || defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
|
||||||
|
#define JSON_UNLOCKED_FILE_IO 1
|
||||||
|
#else
|
||||||
|
#define JSON_UNLOCKED_FILE_IO 0
|
||||||
|
#endif
|
||||||
|
#endif // JSON_NO_IO
|
||||||
|
|
||||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||||
namespace detail
|
namespace detail
|
||||||
{
|
{
|
||||||
@@ -56,18 +76,35 @@ class file_input_adapter
|
|||||||
: m_file(f)
|
: m_file(f)
|
||||||
{
|
{
|
||||||
JSON_ASSERT(m_file != nullptr);
|
JSON_ASSERT(m_file != nullptr);
|
||||||
|
lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// make class move-only
|
// make class move-only
|
||||||
file_input_adapter(const file_input_adapter&) = delete;
|
file_input_adapter(const file_input_adapter&) = delete;
|
||||||
file_input_adapter(file_input_adapter&&) noexcept = default;
|
file_input_adapter(file_input_adapter&& rhs) noexcept
|
||||||
|
: m_file(rhs.m_file)
|
||||||
|
{
|
||||||
|
// the lock is owned by the thread, not the adapter: hand it over
|
||||||
|
rhs.m_file = nullptr;
|
||||||
|
}
|
||||||
file_input_adapter& operator=(const file_input_adapter&) = delete;
|
file_input_adapter& operator=(const file_input_adapter&) = delete;
|
||||||
file_input_adapter& operator=(file_input_adapter&&) = delete;
|
file_input_adapter& operator=(file_input_adapter&&) = delete;
|
||||||
~file_input_adapter() = default;
|
~file_input_adapter()
|
||||||
|
{
|
||||||
|
unlock();
|
||||||
|
}
|
||||||
|
|
||||||
std::char_traits<char>::int_type get_character() noexcept
|
std::char_traits<char>::int_type get_character() noexcept
|
||||||
{
|
{
|
||||||
|
// the file is locked for the lifetime of the adapter, so the
|
||||||
|
// per-character lock of std::fgetc can be skipped
|
||||||
|
#if JSON_UNLOCKED_FILE_IO == 1
|
||||||
|
return getc_unlocked(m_file);
|
||||||
|
#elif JSON_UNLOCKED_FILE_IO == 2
|
||||||
|
return _fgetc_nolock(m_file);
|
||||||
|
#else
|
||||||
return std::fgetc(m_file);
|
return std::fgetc(m_file);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the number of characters successfully read
|
// returns the number of characters successfully read
|
||||||
@@ -78,6 +115,32 @@ class file_input_adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Lock the file once for the whole parse instead of once per character.
|
||||||
|
// The lock is recursive, so fread() and the user's own stdio calls on the
|
||||||
|
// same thread keep working.
|
||||||
|
void lock() noexcept
|
||||||
|
{
|
||||||
|
#if JSON_UNLOCKED_FILE_IO == 1
|
||||||
|
flockfile(m_file);
|
||||||
|
#elif JSON_UNLOCKED_FILE_IO == 2
|
||||||
|
_lock_file(m_file);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlock() noexcept
|
||||||
|
{
|
||||||
|
// a moved-from adapter has no file and holds no lock
|
||||||
|
if (m_file == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#if JSON_UNLOCKED_FILE_IO == 1
|
||||||
|
funlockfile(m_file);
|
||||||
|
#elif JSON_UNLOCKED_FILE_IO == 2
|
||||||
|
_unlock_file(m_file);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/// the file pointer to read from
|
/// the file pointer to read from
|
||||||
std::FILE* m_file;
|
std::FILE* m_file;
|
||||||
};
|
};
|
||||||
@@ -127,15 +190,26 @@ class input_stream_adapter
|
|||||||
input_stream_adapter& operator=(input_stream_adapter&) = delete;
|
input_stream_adapter& operator=(input_stream_adapter&) = delete;
|
||||||
input_stream_adapter& operator=(input_stream_adapter&&) = delete;
|
input_stream_adapter& operator=(input_stream_adapter&&) = delete;
|
||||||
|
|
||||||
#if JSON_PRECISE_STREAM_POSITION
|
|
||||||
input_stream_adapter(input_stream_adapter&& rhs) noexcept
|
input_stream_adapter(input_stream_adapter&& rhs) noexcept
|
||||||
: is(rhs.is), sb(rhs.sb), lookahead(rhs.lookahead)
|
: is(rhs.is), sb(rhs.sb)
|
||||||
|
#if JSON_PRECISE_STREAM_POSITION
|
||||||
|
, lookahead(rhs.lookahead)
|
||||||
|
#endif
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
, token_carry(std::move(rhs.token_carry))
|
||||||
|
, token_segment(rhs.token_segment)
|
||||||
|
, current_in_get_area(rhs.current_in_get_area)
|
||||||
|
, tracking_token(rhs.tracking_token)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
rhs.is = nullptr;
|
rhs.is = nullptr;
|
||||||
rhs.sb = nullptr;
|
rhs.sb = nullptr;
|
||||||
|
#if JSON_PRECISE_STREAM_POSITION
|
||||||
rhs.lookahead = false;
|
rhs.lookahead = false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if JSON_PRECISE_STREAM_POSITION
|
||||||
// Whether the character last returned by get_character() can be given back
|
// Whether the character last returned by get_character() can be given back
|
||||||
// to the input with release_lookahead().
|
// to the input with release_lookahead().
|
||||||
static constexpr bool supports_lookahead = true;
|
static constexpr bool supports_lookahead = true;
|
||||||
@@ -155,17 +229,41 @@ class input_stream_adapter
|
|||||||
sb->sbumpc();
|
sb->sbumpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
// sgetc() refills the get area if it is exhausted
|
||||||
|
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(refill))
|
||||||
|
{
|
||||||
|
save_token_segment();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto res = sb->sgetc();
|
auto res = sb->sgetc();
|
||||||
// set eof manually, as we don't use the istream interface.
|
// set eof manually, as we don't use the istream interface.
|
||||||
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
||||||
{
|
{
|
||||||
// there is nothing to step over next time
|
// there is nothing to step over next time
|
||||||
lookahead = false;
|
lookahead = false;
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
current_in_get_area = false;
|
||||||
|
#endif
|
||||||
is->clear(is->rdstate() | std::ios::eofbit);
|
is->clear(is->rdstate() | std::ios::eofbit);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lookahead = true;
|
lookahead = true;
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(refill))
|
||||||
|
{
|
||||||
|
// the peeked character is at gptr(), unless the streambuf has
|
||||||
|
// no get area
|
||||||
|
after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_end(sb));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current_in_get_area = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -179,13 +277,6 @@ class input_stream_adapter
|
|||||||
lookahead = false;
|
lookahead = false;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
input_stream_adapter(input_stream_adapter&& rhs) noexcept
|
|
||||||
: is(rhs.is), sb(rhs.sb)
|
|
||||||
{
|
|
||||||
rhs.is = nullptr;
|
|
||||||
rhs.sb = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// std::istream/std::streambuf use std::char_traits<char>::to_int_type, to
|
// std::istream/std::streambuf use std::char_traits<char>::to_int_type, to
|
||||||
// ensure that std::char_traits<char>::eof() and the character 0xFF do not
|
// ensure that std::char_traits<char>::eof() and the character 0xFF do not
|
||||||
// end up as the same value, e.g., 0xFFFFFFFF.
|
// end up as the same value, e.g., 0xFFFFFFFF.
|
||||||
@@ -194,12 +285,37 @@ class input_stream_adapter
|
|||||||
// stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION.
|
// stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION.
|
||||||
std::char_traits<char>::int_type get_character()
|
std::char_traits<char>::int_type get_character()
|
||||||
{
|
{
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
// sbumpc() refills the get area if it is exhausted
|
||||||
|
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(refill))
|
||||||
|
{
|
||||||
|
save_token_segment();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto res = sb->sbumpc();
|
auto res = sb->sbumpc();
|
||||||
// set eof manually, as we don't use the istream interface.
|
// set eof manually, as we don't use the istream interface.
|
||||||
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
||||||
{
|
{
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
current_in_get_area = false;
|
||||||
|
#endif
|
||||||
is->clear(is->rdstate() | std::ios::eofbit);
|
is->clear(is->rdstate() | std::ios::eofbit);
|
||||||
}
|
}
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
else if (JSON_HEDLEY_UNLIKELY(refill))
|
||||||
|
{
|
||||||
|
// the consumed character is at gptr() - 1, unless the streambuf
|
||||||
|
// has no get area and returned it from uflow()
|
||||||
|
const char* const next = streambuf_access::get_ptr(sb);
|
||||||
|
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current_in_get_area = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -218,7 +334,165 @@ class input_stream_adapter
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
// The lexer may scan the characters in the get area of the streambuf
|
||||||
|
// directly (SWAR fast paths). The get area is only a window into the
|
||||||
|
// input: it ends wherever the streambuf's buffer ends, and is refilled by
|
||||||
|
// the next get_character(). The lexer therefore keeps counting the
|
||||||
|
// position itself and treats the end of the window as "unknown", not as
|
||||||
|
// the end of the input.
|
||||||
|
static constexpr bool supports_bulk_scan = true;
|
||||||
|
static constexpr bool bulk_window = true;
|
||||||
|
|
||||||
|
// Pointer to the next unread character; only valid when
|
||||||
|
// bulk_remaining() > 0. The character before it is the one last returned
|
||||||
|
// by get_character().
|
||||||
|
const char* bulk_data() const
|
||||||
|
{
|
||||||
|
return current_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of unread characters in the get area.
|
||||||
|
std::size_t bulk_remaining() const
|
||||||
|
{
|
||||||
|
if (!current_in_get_area)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return static_cast<std::size_t>(streambuf_access::get_end(sb) - current_end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume @a n characters previously inspected via bulk_data(); the last
|
||||||
|
// of them becomes the character last returned by get_character().
|
||||||
|
void bulk_skip(std::size_t n)
|
||||||
|
{
|
||||||
|
JSON_ASSERT(n <= bulk_remaining());
|
||||||
|
// gbump() takes an int, but a get area (e.g., of a std::stringbuf) can
|
||||||
|
// be larger
|
||||||
|
constexpr auto max_bump = static_cast<std::size_t>((std::numeric_limits<int>::max)());
|
||||||
|
for (; n > max_bump; n -= max_bump)
|
||||||
|
{
|
||||||
|
streambuf_access::bump(sb, static_cast<int>(max_bump));
|
||||||
|
}
|
||||||
|
streambuf_access::bump(sb, static_cast<int>(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The adapter keeps the current token for the lexer's error messages: the
|
||||||
|
// part in the get area is only remembered as a pointer, and copied out
|
||||||
|
// when the get area is about to be refilled.
|
||||||
|
static constexpr bool supports_token_tracking = true;
|
||||||
|
|
||||||
|
// Start a token with the characters read from now on. Until this is
|
||||||
|
// called, nothing is tracked (the binary readers never call it).
|
||||||
|
void begin_token() noexcept
|
||||||
|
{
|
||||||
|
tracking_token = true;
|
||||||
|
token_carry.clear();
|
||||||
|
token_segment = current_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new token with the character @a c last returned by
|
||||||
|
// get_character().
|
||||||
|
void mark_token_start(std::char_traits<char>::int_type c)
|
||||||
|
{
|
||||||
|
tracking_token = true;
|
||||||
|
token_carry.clear();
|
||||||
|
if (current_in_get_area)
|
||||||
|
{
|
||||||
|
token_segment = current_end() - 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token_segment = nullptr;
|
||||||
|
token_carry.push_back(std::char_traits<char>::to_char_type(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the characters read since mark_token_start() to @a out, without the
|
||||||
|
// last one if @a drop_last.
|
||||||
|
void copy_token(std::vector<char>& out, bool drop_last) const
|
||||||
|
{
|
||||||
|
out = token_carry;
|
||||||
|
if (token_segment != nullptr)
|
||||||
|
{
|
||||||
|
out.insert(out.end(), token_segment, current_end());
|
||||||
|
}
|
||||||
|
if (drop_last && !out.empty())
|
||||||
|
{
|
||||||
|
out.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
// Reads the protected get-area pointers of any std::streambuf. Naming the
|
||||||
|
// members through a derived class makes forming the member pointers
|
||||||
|
// accessible; calling them on another streambuf object is then ordinary
|
||||||
|
// member access through a pointer to member.
|
||||||
|
struct streambuf_access : std::streambuf
|
||||||
|
{
|
||||||
|
static const char* get_ptr(std::streambuf* s)
|
||||||
|
{
|
||||||
|
return (s->*(&streambuf_access::gptr))();
|
||||||
|
}
|
||||||
|
static const char* get_end(std::streambuf* s)
|
||||||
|
{
|
||||||
|
return (s->*(&streambuf_access::egptr))();
|
||||||
|
}
|
||||||
|
static const char* get_begin(std::streambuf* s)
|
||||||
|
{
|
||||||
|
return (s->*(&streambuf_access::eback))();
|
||||||
|
}
|
||||||
|
static void bump(std::streambuf* s, int n)
|
||||||
|
{
|
||||||
|
(s->*(&streambuf_access::gbump))(n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// one past the character last returned by get_character(), if that is in
|
||||||
|
// the get area
|
||||||
|
const char* current_end() const
|
||||||
|
{
|
||||||
|
#if JSON_PRECISE_STREAM_POSITION
|
||||||
|
// the character is peeked at gptr() and not stepped over yet, unless
|
||||||
|
// it was given back with release_lookahead()
|
||||||
|
return streambuf_access::get_ptr(sb) + (lookahead ? 1 : 0);
|
||||||
|
#else
|
||||||
|
return streambuf_access::get_ptr(sb);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// the get area is about to be replaced: copy the token's part in it
|
||||||
|
void save_token_segment()
|
||||||
|
{
|
||||||
|
if (token_segment != nullptr)
|
||||||
|
{
|
||||||
|
token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb));
|
||||||
|
token_segment = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// character @a c was just read into a new get area (@a in_get_area) or
|
||||||
|
// returned without one
|
||||||
|
void after_refill(std::char_traits<char>::int_type c, bool in_get_area)
|
||||||
|
{
|
||||||
|
current_in_get_area = in_get_area;
|
||||||
|
if (!tracking_token)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (in_get_area)
|
||||||
|
{
|
||||||
|
token_segment = current_end() - 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token_carry.push_back(std::char_traits<char>::to_char_type(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if JSON_PRECISE_STREAM_POSITION
|
#if JSON_PRECISE_STREAM_POSITION
|
||||||
// Step over the character last returned by get_character(). The character
|
// Step over the character last returned by get_character(). The character
|
||||||
// has already been peeked successfully, so for every streambuf with a get
|
// has already been peeked successfully, so for every streambuf with a get
|
||||||
@@ -240,6 +514,16 @@ class input_stream_adapter
|
|||||||
/// whether get_character() peeked a character that is not consumed yet
|
/// whether get_character() peeked a character that is not consumed yet
|
||||||
bool lookahead = false;
|
bool lookahead = false;
|
||||||
#endif
|
#endif
|
||||||
|
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
/// the part of the current token that was in earlier get areas
|
||||||
|
std::vector<char> token_carry{}; // NOLINT(readability-redundant-member-init)
|
||||||
|
/// start of the current token's part in the get area, or nullptr
|
||||||
|
const char* token_segment = nullptr;
|
||||||
|
/// whether the character last returned by get_character() is in the get area
|
||||||
|
bool current_in_get_area = false;
|
||||||
|
/// whether a token is tracked at all; the binary readers never start one
|
||||||
|
bool tracking_token = false;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
#endif // JSON_NO_IO
|
#endif // JSON_NO_IO
|
||||||
|
|
||||||
@@ -314,22 +598,17 @@ class iterator_input_adapter
|
|||||||
return get_consumed_count() + eof_reads;
|
return get_consumed_count() + eof_reads;
|
||||||
}
|
}
|
||||||
|
|
||||||
// number of newlines in the consumed range [first_index, last_index); if
|
// number of newlines in the consumed range [0, last_index); @a line_start
|
||||||
// there is one, @a line_start is set to the index after the last of them,
|
// is set to the index after the last of them (0 if there is none); only
|
||||||
// otherwise it is left unchanged; only valid when supports_seek
|
// valid when supports_seek
|
||||||
std::size_t count_consumed_lines(std::size_t first_index, std::size_t last_index, std::size_t& line_start) const
|
std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
|
||||||
{
|
{
|
||||||
using difference_type = typename std::iterator_traits<IteratorType>::difference_type;
|
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
|
||||||
const auto from = std::next(begin, static_cast<difference_type>(first_index));
|
|
||||||
const auto to = std::next(from, static_cast<difference_type>(last_index - first_index));
|
|
||||||
const auto newline = char_traits<char_type>::to_char_type('\n');
|
const auto newline = char_traits<char_type>::to_char_type('\n');
|
||||||
using reverse_it = std::reverse_iterator<IteratorType>;
|
using reverse_it = std::reverse_iterator<IteratorType>;
|
||||||
const auto last_newline = std::find(reverse_it(to), reverse_it(from), newline);
|
const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
|
||||||
if (last_newline.base() != from)
|
|
||||||
{
|
|
||||||
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
|
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
|
||||||
}
|
return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
|
||||||
return static_cast<std::size_t>(std::count(from, last_newline.base(), newline));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append the already-consumed characters in the half-open range
|
// append the already-consumed characters in the half-open range
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -329,11 +327,8 @@ 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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -166,6 +166,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect whether an input adapter keeps the current token itself and can
|
||||||
|
// hand it out on demand (see input_stream_adapter::supports_token_tracking),
|
||||||
|
// so the lexer need not copy every scanned character into token_string.
|
||||||
|
template<typename InputAdapterType>
|
||||||
|
using detect_supports_token_tracking = decltype(InputAdapterType::supports_token_tracking);
|
||||||
|
|
||||||
|
template<typename InputAdapterType>
|
||||||
|
constexpr bool input_adapter_supports_token_tracking(std::true_type /*detected*/)
|
||||||
|
{
|
||||||
|
return InputAdapterType::supports_token_tracking;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename InputAdapterType>
|
||||||
|
constexpr bool input_adapter_supports_token_tracking(std::false_type /*detected*/)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect whether the contiguous block of a bulk-scan adapter is only a window
|
||||||
|
// into the input (see input_stream_adapter::bulk_window): its end is not the
|
||||||
|
// end of the input, so a token that touches it must be finished character by
|
||||||
|
// character.
|
||||||
|
template<typename InputAdapterType>
|
||||||
|
using detect_bulk_window = decltype(InputAdapterType::bulk_window);
|
||||||
|
|
||||||
|
template<typename InputAdapterType>
|
||||||
|
constexpr bool input_adapter_bulk_window(std::true_type /*detected*/)
|
||||||
|
{
|
||||||
|
return InputAdapterType::bulk_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename InputAdapterType>
|
||||||
|
constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief lexical analysis
|
@brief lexical analysis
|
||||||
|
|
||||||
@@ -184,9 +221,20 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
/// whether the last read token can be reconstructed from the input adapter
|
/// whether the last read token can be reconstructed from the input adapter
|
||||||
/// on demand (in error paths) instead of being copied on every scanned
|
/// on demand (in error paths) instead of being copied on every scanned
|
||||||
/// character; see input_adapter_supports_seek
|
/// character; see input_adapter_supports_seek
|
||||||
static constexpr bool lazy_token_string =
|
static constexpr bool seekable =
|
||||||
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
|
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
|
||||||
|
|
||||||
|
/// whether the adapter keeps the token itself (mark_token_start() /
|
||||||
|
/// copy_token()); see input_adapter_supports_token_tracking
|
||||||
|
static constexpr bool adapter_token =
|
||||||
|
!seekable && input_adapter_supports_token_tracking<InputAdapterType>(is_detected<detect_supports_token_tracking, InputAdapterType> {});
|
||||||
|
|
||||||
|
/// whether the last read token is obtained from the input adapter on
|
||||||
|
/// demand (in error paths) instead of being copied on every scanned
|
||||||
|
/// character: seekable adapters reconstruct it from the consumed input,
|
||||||
|
/// token-tracking adapters keep it themselves
|
||||||
|
static constexpr bool lazy_token_string = seekable || adapter_token;
|
||||||
|
|
||||||
/// whether the read position is derived from the input adapter on demand
|
/// whether the read position is derived from the input adapter on demand
|
||||||
/// instead of being counted on every scanned character. Seekable adapters
|
/// instead of being counted on every scanned character. Seekable adapters
|
||||||
/// know how much input they consumed and can revisit it, so
|
/// know how much input they consumed and can revisit it, so
|
||||||
@@ -194,7 +242,7 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
/// and line and column are recomputed from the consumed input only when
|
/// and line and column are recomputed from the consumed input only when
|
||||||
/// get_position() is called (errors). Counting per character kept two
|
/// get_position() is called (errors). Counting per character kept two
|
||||||
/// increments and a newline check in the hot path of every get().
|
/// increments and a newline check in the hot path of every get().
|
||||||
static constexpr bool lazy_position = lazy_token_string;
|
static constexpr bool lazy_position = seekable;
|
||||||
|
|
||||||
/// whether a simulated unget can be passed on to the input adapter, which
|
/// whether a simulated unget can be passed on to the input adapter, which
|
||||||
/// then leaves the character in the input; see
|
/// then leaves the character in the input; see
|
||||||
@@ -203,14 +251,17 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
input_adapter_supports_lookahead<InputAdapterType>(is_detected<detect_supports_lookahead, InputAdapterType> {});
|
input_adapter_supports_lookahead<InputAdapterType>(is_detected<detect_supports_lookahead, InputAdapterType> {});
|
||||||
|
|
||||||
/// whether string scanning may bulk-consume runs of ordinary characters
|
/// whether string scanning may bulk-consume runs of ordinary characters
|
||||||
/// directly from a contiguous input buffer (SWAR fast path). This requires
|
/// directly from a contiguous input buffer (SWAR fast path). Without
|
||||||
/// the token to be reconstructible lazily (lazy_token_string), so bypassing
|
/// lazy_token_string / lazy_position, the bulk paths copy the consumed
|
||||||
/// the per-character capture in get() cannot lose error diagnostics. It
|
/// characters into token_string and advance the position counters
|
||||||
/// also implies lazy_position, so the bulk paths need not advance the
|
/// themselves (see bulk_consume).
|
||||||
/// position counters.
|
|
||||||
static constexpr bool bulk_scan =
|
static constexpr bool bulk_scan =
|
||||||
lazy_token_string
|
input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
|
||||||
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
|
|
||||||
|
/// whether the contiguous block is only a window into the input, so that
|
||||||
|
/// its end does not end a token; see input_adapter_bulk_window
|
||||||
|
static constexpr bool bulk_window =
|
||||||
|
input_adapter_bulk_window<InputAdapterType>(is_detected<detect_bulk_window, InputAdapterType> {});
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using token_type = typename lexer_base<BasicJsonType>::token_type;
|
using token_type = typename lexer_base<BasicJsonType>::token_type;
|
||||||
@@ -219,7 +270,11 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
: ia(std::move(adapter))
|
: ia(std::move(adapter))
|
||||||
, ignore_comments(ignore_comments_)
|
, ignore_comments(ignore_comments_)
|
||||||
, discard_number_values(discard_number_values_)
|
, discard_number_values(discard_number_values_)
|
||||||
{}
|
{
|
||||||
|
// the token string of an error before the first token is everything
|
||||||
|
// read so far
|
||||||
|
begin_token(std::integral_constant<bool, adapter_token> {});
|
||||||
|
}
|
||||||
|
|
||||||
// deleted because of pointer members
|
// deleted because of pointer members
|
||||||
lexer(const lexer&) = delete;
|
lexer(const lexer&) = delete;
|
||||||
@@ -236,8 +291,8 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
/// contiguous input: try to decode the 4 hex digits following `\\u`
|
/// contiguous input: try to decode the 4 hex digits following `\\u`
|
||||||
/// directly from the input buffer via hex_codepoint(), instead of 4 calls
|
/// directly from the input buffer via hex_codepoint(), instead of 4 calls
|
||||||
/// to get(). On success, advances the adapter exactly as those 4 get()
|
/// to get(). On success, advances the adapter exactly as those 4 get()
|
||||||
/// calls would (bulk_scan implies lazy_position, so there are no position
|
/// calls would (including the position and token bookkeeping of a
|
||||||
/// counters to advance) and leaves @a current holding the last of
|
/// streaming adapter, see bulk_consume) and leaves @a current holding the last of
|
||||||
/// the 4 digits, just as the last such get() would; the codepoint is
|
/// the 4 digits, just as the last such get() would; the codepoint is
|
||||||
/// written to @a out. Makes no state change and returns false for a
|
/// written to @a out. Makes no state change and returns false for a
|
||||||
/// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not
|
/// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not
|
||||||
@@ -256,7 +311,7 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ia.bulk_skip(4);
|
bulk_consume(reinterpret_cast<const char*>(raw), 4);
|
||||||
current = char_traits<char_type>::to_int_type(raw[3]);
|
current = char_traits<char_type>::to_int_type(raw[3]);
|
||||||
out = codepoint;
|
out = codepoint;
|
||||||
return true;
|
return true;
|
||||||
@@ -385,7 +440,36 @@ class lexer : public lexer_base<BasicJsonType>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
||||||
ia.bulk_skip(pos);
|
bulk_consume(reinterpret_cast<const char*>(data), pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// consume @a n characters at @a data (the adapter's bulk_data()) and do
|
||||||
|
/// the bookkeeping get() would have done for them; the characters are
|
||||||
|
/// never newlines
|
||||||
|
void bulk_consume(const char* data, std::size_t n)
|
||||||
|
{
|
||||||
|
bulk_track_position(n, std::integral_constant<bool, lazy_position> {});
|
||||||
|
bulk_track_token(data, n, std::integral_constant<bool, lazy_token_string> {});
|
||||||
|
ia.bulk_skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// seekable adapter: the position is derived from the adapter
|
||||||
|
void bulk_track_position(std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
|
||||||
|
|
||||||
|
/// streaming adapter: count the characters
|
||||||
|
void bulk_track_position(std::size_t n, std::false_type /*lazy*/) noexcept
|
||||||
|
{
|
||||||
|
position.chars_read_total += n;
|
||||||
|
position.chars_read_current_line += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the token is obtained from the adapter
|
||||||
|
void bulk_track_token(const char* /*data*/, std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
|
||||||
|
|
||||||
|
/// streaming adapter: capture the characters
|
||||||
|
void bulk_track_token(const char* data, std::size_t n, std::false_type /*lazy*/)
|
||||||
|
{
|
||||||
|
token_string.insert(token_string.end(), data, data + n);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// streaming input: no bulk fast path
|
/// streaming input: no bulk fast path
|
||||||
@@ -490,16 +574,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 +600,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 +614,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1617,6 +1687,10 @@ scan_number_done:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const std::size_t len = i;
|
const std::size_t len = i;
|
||||||
|
if (number_may_continue(len == avail, std::integral_constant<bool, bulk_window> {}))
|
||||||
|
{
|
||||||
|
return token_type::uninitialized;
|
||||||
|
}
|
||||||
|
|
||||||
// reset() records where this token starts (for diagnostics), so it has
|
// reset() records where this token starts (for diagnostics), so it has
|
||||||
// to run before the input position advances below
|
// to run before the input position advances below
|
||||||
@@ -1634,7 +1708,8 @@ scan_number_done:
|
|||||||
const token_type integer_result = convert_integer(number_type, data, data + len);
|
const token_type integer_result = convert_integer(number_type, data, data + len);
|
||||||
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
|
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
|
||||||
{
|
{
|
||||||
ia.bulk_skip(len - 1);
|
bulk_consume(data + 1, len - 1);
|
||||||
|
read_number_terminator(std::integral_constant<bool, bulk_window> {});
|
||||||
return integer_result;
|
return integer_result;
|
||||||
}
|
}
|
||||||
// The value does not fit an integer, so this token converts as a
|
// The value does not fit an integer, so this token converts as a
|
||||||
@@ -1650,11 +1725,37 @@ scan_number_done:
|
|||||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||||
decimal_point_position = dot_index;
|
decimal_point_position = dot_index;
|
||||||
|
|
||||||
ia.bulk_skip(len - 1);
|
bulk_consume(data + 1, len - 1);
|
||||||
|
read_number_terminator(std::integral_constant<bool, bulk_window> {});
|
||||||
|
|
||||||
return convert_number(number_type, mantissa_end);
|
return convert_number(number_type, mantissa_end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// window adapter: a number that reaches the end of the window may continue
|
||||||
|
/// after it, so scan_number() has to read it character by character
|
||||||
|
static constexpr bool number_may_continue(bool at_end, std::true_type /*window*/) noexcept
|
||||||
|
{
|
||||||
|
return at_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the end of the block is the end of the input, which ends the number
|
||||||
|
static constexpr bool number_may_continue(bool /*at_end*/, std::false_type /*window*/) noexcept
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// window adapter: read the character after a bulk-scanned number and unget
|
||||||
|
/// it, as scan_number() does. The adapter is a stream that is handed back to
|
||||||
|
/// the user, who must find the same input consumed either way.
|
||||||
|
void read_number_terminator(std::true_type /*window*/)
|
||||||
|
{
|
||||||
|
get();
|
||||||
|
unget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the input is not handed back, so the terminator can stay unread
|
||||||
|
void read_number_terminator(std::false_type /*window*/) const noexcept {}
|
||||||
|
|
||||||
/// contiguous input: try the number fast path, else the byte-path scanner
|
/// contiguous input: try the number fast path, else the byte-path scanner
|
||||||
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
||||||
{
|
{
|
||||||
@@ -1710,11 +1811,30 @@ scan_number_done:
|
|||||||
/// seekable adapter: remember where the current token starts so it can be
|
/// seekable adapter: remember where the current token starts so it can be
|
||||||
/// reconstructed from the input on error; current has already been
|
/// reconstructed from the input on error; current has already been
|
||||||
/// consumed, hence the -1
|
/// consumed, hence the -1
|
||||||
void note_token_start(std::true_type /*lazy*/) noexcept
|
void note_token_start(std::true_type /*lazy*/)
|
||||||
|
{
|
||||||
|
note_token_start_lazy(std::integral_constant<bool, adapter_token> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void note_token_start_lazy(std::false_type /*adapter_token*/) noexcept
|
||||||
{
|
{
|
||||||
token_string_start = ia.get_consumed_count() - 1;
|
token_string_start = ia.get_consumed_count() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// token-tracking adapter: start tracking the token at the first character
|
||||||
|
void begin_token(std::true_type /*adapter_token*/) noexcept
|
||||||
|
{
|
||||||
|
ia.begin_token();
|
||||||
|
}
|
||||||
|
|
||||||
|
void begin_token(std::false_type /*adapter_token*/) const noexcept {}
|
||||||
|
|
||||||
|
/// token-tracking adapter: the token starts with the current character
|
||||||
|
void note_token_start_lazy(std::true_type /*adapter_token*/)
|
||||||
|
{
|
||||||
|
ia.mark_token_start(current);
|
||||||
|
}
|
||||||
|
|
||||||
/// streaming adapter: start copying the token eagerly, beginning with the
|
/// streaming adapter: start copying the token eagerly, beginning with the
|
||||||
/// already-read first character
|
/// already-read first character
|
||||||
void note_token_start(std::false_type /*lazy*/) noexcept
|
void note_token_start(std::false_type /*lazy*/) noexcept
|
||||||
@@ -2012,25 +2132,17 @@ scan_number_done:
|
|||||||
return position.chars_read_total;
|
return position.chars_read_total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// seekable adapter: count the lines in the consumed input. Only the
|
/// seekable adapter: count the lines in the consumed input; O(input read
|
||||||
/// input consumed since the previous call is scanned, so a parse that
|
/// so far), but only called on errors
|
||||||
/// reports many errors (a SAX parser recovering from them) stays linear
|
|
||||||
/// in the input instead of rescanning it for every error.
|
|
||||||
position_t get_position_impl(std::true_type /*lazy*/) const
|
position_t get_position_impl(std::true_type /*lazy*/) const
|
||||||
{
|
{
|
||||||
position_t result;
|
position_t result;
|
||||||
result.chars_read_total = get_chars_read_total();
|
result.chars_read_total = get_chars_read_total();
|
||||||
// reads at EOF count as characters but are not part of the input
|
// reads at EOF count as characters but are not part of the input
|
||||||
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
|
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
|
||||||
if (JSON_HEDLEY_UNLIKELY(consumed < position_cache.consumed))
|
std::size_t line_start = 0;
|
||||||
{
|
result.lines_read = ia.count_consumed_lines(consumed, line_start);
|
||||||
// the consumed input never shrinks; recount from the start if it did
|
result.chars_read_current_line = result.chars_read_total - line_start;
|
||||||
position_cache = {};
|
|
||||||
}
|
|
||||||
position_cache.lines += ia.count_consumed_lines(position_cache.consumed, consumed, position_cache.line_start);
|
|
||||||
position_cache.consumed = consumed;
|
|
||||||
result.lines_read = position_cache.lines;
|
|
||||||
result.chars_read_current_line = result.chars_read_total - position_cache.line_start;
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2043,6 +2155,20 @@ scan_number_done:
|
|||||||
public:
|
public:
|
||||||
/// seekable adapter: rebuild the last read token from the input on demand
|
/// seekable adapter: rebuild the last read token from the input on demand
|
||||||
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
|
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
|
||||||
|
{
|
||||||
|
return collect_token_chars_lazy(out, std::integral_constant<bool, adapter_token> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// token-tracking adapter: the adapter hands out everything read since
|
||||||
|
/// mark_token_start(), except a pending unget (see below)
|
||||||
|
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::true_type /*adapter_token*/) const
|
||||||
|
{
|
||||||
|
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
|
||||||
|
ia.copy_token(out, pending_real_unget);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::false_type /*adapter_token*/) const
|
||||||
{
|
{
|
||||||
// a pending unget of a real (non-EOF) character means that character
|
// a pending unget of a real (non-EOF) character means that character
|
||||||
// was consumed from the input but is not part of the token; EOF is
|
// was consumed from the input but is not part of the token; EOF is
|
||||||
@@ -2164,9 +2290,8 @@ scan_number_done:
|
|||||||
Pretty-printed JSON indents every line with a run of spaces, which the loop
|
Pretty-printed JSON indents every line with a run of spaces, which the loop
|
||||||
in skip_whitespace() would otherwise read one character at a time. The
|
in skip_whitespace() would otherwise read one character at a time. The
|
||||||
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
|
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
|
||||||
other whitespace are left to that loop. Skipping bypasses get(), which is
|
other whitespace are left to that loop. Skipping bypasses get();
|
||||||
fine because bulk_scan implies lazy_position: the position is derived from
|
bulk_consume() does its bookkeeping.
|
||||||
the adapter, not counted per character.
|
|
||||||
*/
|
*/
|
||||||
void skip_indentation(std::true_type /*bulk*/)
|
void skip_indentation(std::true_type /*bulk*/)
|
||||||
{
|
{
|
||||||
@@ -2189,7 +2314,7 @@ scan_number_done:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ia.bulk_skip(pos);
|
bulk_consume(data, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// streaming input: no bulk fast path
|
/// streaming input: no bulk fast path
|
||||||
@@ -2319,574 +2444,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;
|
||||||
|
|
||||||
@@ -2906,19 +2463,6 @@ scan_number_done:
|
|||||||
/// that unget() can restore the column instead of leaving it at 0
|
/// that unget() can restore the column instead of leaving it at 0
|
||||||
std::size_t chars_read_before_newline = 0;
|
std::size_t chars_read_before_newline = 0;
|
||||||
|
|
||||||
/// seekable adapter: what get_position() counted last, so that the next
|
|
||||||
/// call only scans the input consumed since
|
|
||||||
struct position_cache_t
|
|
||||||
{
|
|
||||||
/// consumed characters scanned so far
|
|
||||||
std::size_t consumed = 0;
|
|
||||||
/// newlines among them
|
|
||||||
std::size_t lines = 0;
|
|
||||||
/// index after the last of these newlines
|
|
||||||
std::size_t line_start = 0;
|
|
||||||
};
|
|
||||||
mutable position_cache_t position_cache {};
|
|
||||||
|
|
||||||
/// raw input token string for error messages; only populated for streaming
|
/// raw input token string for error messages; only populated for streaming
|
||||||
/// adapters (seekable adapters reconstruct it lazily via token_string_start)
|
/// adapters (seekable adapters reconstruct it lazily via token_string_start)
|
||||||
std::vector<char_type> token_string {};
|
std::vector<char_type> token_string {};
|
||||||
@@ -2939,13 +2483,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;
|
||||||
|
|||||||
@@ -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,544 +492,8 @@ 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,9 +19,9 @@
|
|||||||
#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_UNLOCKED_FILE_IO
|
||||||
#undef JSON_DTOA_SSE2
|
#undef JSON_DTOA_SSE2
|
||||||
#undef JSON_DTOA_NEON
|
#undef JSON_DTOA_NEON
|
||||||
|
|
||||||
@@ -48,6 +48,8 @@
|
|||||||
#undef JSON_STRICT_BINARY_UTF8
|
#undef JSON_STRICT_BINARY_UTF8
|
||||||
#undef JSON_DELETE_DEPRECATED_FUNCTIONS
|
#undef JSON_DELETE_DEPRECATED_FUNCTIONS
|
||||||
#undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS
|
#undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS
|
||||||
|
#undef JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#undef JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <nlohmann/thirdparty/hedley/hedley_undef.hpp>
|
#include <nlohmann/thirdparty/hedley/hedley_undef.hpp>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
Generated
+113664
File diff suppressed because it is too large.
Load diff
Generated
+594
-2478
File diff suppressed because it is too large.
Load diff
Generated
+26
-4
@@ -71,6 +71,14 @@
|
|||||||
#define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0
|
#define JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#define JSON_DISABLE_STREAM_BULK_SCAN 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
|
||||||
|
#endif
|
||||||
|
|
||||||
#if JSON_DIAGNOSTICS
|
#if JSON_DIAGNOSTICS
|
||||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
||||||
#else
|
#else
|
||||||
@@ -119,14 +127,26 @@
|
|||||||
#define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS
|
#define NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN _dsbs
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO _dufio
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||||
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Construct the namespace ABI tags component
|
// Construct the namespace ABI tags component
|
||||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h) json_abi ## a ## b ## c ## d ## e ## f ## g ## h
|
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j
|
||||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h) \
|
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j) \
|
||||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h)
|
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j)
|
||||||
|
|
||||||
#define NLOHMANN_JSON_ABI_TAGS \
|
#define NLOHMANN_JSON_ABI_TAGS \
|
||||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||||
@@ -137,7 +157,9 @@
|
|||||||
NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \
|
NLOHMANN_JSON_ABI_TAG_PRECISE_STREAM_POSITION, \
|
||||||
NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \
|
NLOHMANN_JSON_ABI_TAG_STRICT_NUL_HANDLING, \
|
||||||
NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \
|
NLOHMANN_JSON_ABI_TAG_STRICT_BINARY_UTF8, \
|
||||||
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS)
|
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO)
|
||||||
|
|
||||||
// Construct the namespace version component
|
// Construct the namespace version component
|
||||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -285,6 +285,10 @@ if (JSON_TestSimdutf)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# test-file_input checks from a second thread that parsing from a FILE* releases the file's lock
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
json_test_set_test_options("test-file_input;test-file_input_unlocked_io_disabled" LINK_LIBRARIES Threads::Threads)
|
||||||
|
|
||||||
# *DO* use json_test_set_test_options() above this line
|
# *DO* use json_test_set_test_options() above this line
|
||||||
|
|
||||||
json_test_should_build_32bit_test(json_32bit_test json_32bit_test_only "${JSON_32bitTest}")
|
json_test_should_build_32bit_test(json_32bit_test json_32bit_test_only "${JSON_32bitTest}")
|
||||||
@@ -369,6 +373,33 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp
|
|||||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# test reading from a std::istream again without scanning the get area in bulk
|
||||||
|
json_test_set_test_options(test-stream-bulk-scan_disabled
|
||||||
|
COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||||
|
)
|
||||||
|
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
|
||||||
|
NAME test-stream-bulk-scan_disabled
|
||||||
|
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||||
|
)
|
||||||
|
|
||||||
|
# test reading from a std::istream again with JSON_PRECISE_STREAM_POSITION
|
||||||
|
json_test_set_test_options(test-stream-bulk-scan_precise
|
||||||
|
COMPILE_DEFINITIONS JSON_PRECISE_STREAM_POSITION=1
|
||||||
|
)
|
||||||
|
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
|
||||||
|
NAME test-stream-bulk-scan_precise
|
||||||
|
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||||
|
)
|
||||||
|
|
||||||
|
# test reading from a FILE* again with the locking stdio functions
|
||||||
|
json_test_set_test_options(test-file_input_unlocked_io_disabled
|
||||||
|
COMPILE_DEFINITIONS JSON_DISABLE_UNLOCKED_FILE_IO=1
|
||||||
|
)
|
||||||
|
json_test_add_test_for(src/unit-file_input.cpp
|
||||||
|
NAME test-file_input_unlocked_io_disabled
|
||||||
|
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||||
|
)
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# *DO NOT* use json_test_set_test_options() below this line
|
# *DO NOT* use json_test_set_test_options() below this line
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ foreach(test default noversion)
|
|||||||
target_compile_definitions(abi_config_${test}_all_tags PRIVATE
|
target_compile_definitions(abi_config_${test}_all_tags PRIVATE
|
||||||
JSON_DIAGNOSTICS=1
|
JSON_DIAGNOSTICS=1
|
||||||
JSON_DIAGNOSTIC_POSITIONS=1
|
JSON_DIAGNOSTIC_POSITIONS=1
|
||||||
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1)
|
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1
|
||||||
|
JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||||
|
JSON_DISABLE_UNLOCKED_FILE_IO=1)
|
||||||
target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main)
|
target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main)
|
||||||
add_test(
|
add_test(
|
||||||
NAME test-abi_config_${test}_all_tags
|
NAME test-abi_config_${test}_all_tags
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ TEST_CASE("default namespace")
|
|||||||
expected += "_ekmo";
|
expected += "_ekmo";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
expected += "_dsbs";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
expected += "_dufio";
|
||||||
|
#endif
|
||||||
|
|
||||||
expected += "_v" STRINGIZE(NLOHMANN_JSON_VERSION_MAJOR);
|
expected += "_v" STRINGIZE(NLOHMANN_JSON_VERSION_MAJOR);
|
||||||
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_MINOR);
|
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_MINOR);
|
||||||
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_PATCH) "::basic_json";
|
expected += "_" STRINGIZE(NLOHMANN_JSON_VERSION_PATCH) "::basic_json";
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ TEST_CASE("default namespace without version component")
|
|||||||
expected += "_ekmo";
|
expected += "_ekmo";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
expected += "_dsbs";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DISABLE_UNLOCKED_FILE_IO
|
||||||
|
expected += "_dufio";
|
||||||
|
#endif
|
||||||
|
|
||||||
expected += "::basic_json";
|
expected += "::basic_json";
|
||||||
|
|
||||||
// fallback for Clang
|
// fallback for Clang
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
|
|
||||||
#include <benchmark/benchmark.h>
|
#include <benchmark/benchmark.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <fstream>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <fstream>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <test_data.hpp>
|
#include <test_data.hpp>
|
||||||
|
|
||||||
@@ -51,6 +53,65 @@ BENCHMARK_CAPTURE(ParseFile, signed_ints, TEST_DATA_DIRECTORY "/regression
|
|||||||
BENCHMARK_CAPTURE(ParseFile, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
|
BENCHMARK_CAPTURE(ParseFile, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
|
||||||
BENCHMARK_CAPTURE(ParseFile, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
|
BENCHMARK_CAPTURE(ParseFile, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// parse JSON from a FILE*
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static void ParseFILE(benchmark::State& state, const char* filename)
|
||||||
|
{
|
||||||
|
while (state.KeepRunning())
|
||||||
|
{
|
||||||
|
state.PauseTiming();
|
||||||
|
std::FILE* f = std::fopen(filename, "rb");
|
||||||
|
auto* j = new json();
|
||||||
|
state.ResumeTiming();
|
||||||
|
|
||||||
|
*j = json::parse(f);
|
||||||
|
|
||||||
|
state.PauseTiming();
|
||||||
|
std::fclose(f);
|
||||||
|
delete j;
|
||||||
|
state.ResumeTiming();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||||
|
state.SetBytesProcessed(state.iterations() * file.tellg());
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(ParseFILE, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||||
|
BENCHMARK_CAPTURE(ParseFILE, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||||
|
BENCHMARK_CAPTURE(ParseFILE, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// read many small values from one stream (NDJSON)
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
static void ParseStreamValues(benchmark::State& state, bool numbers)
|
||||||
|
{
|
||||||
|
std::string input;
|
||||||
|
for (int i = 0; i < 10000; ++i)
|
||||||
|
{
|
||||||
|
input += numbers ? std::to_string(i * 7919) + " "
|
||||||
|
: R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","active":true,"score":)" + std::to_string(i * 0.25) + "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
std::istringstream is(input);
|
||||||
|
json j;
|
||||||
|
std::size_t count = 0;
|
||||||
|
while (!(is >> std::ws).eof())
|
||||||
|
{
|
||||||
|
is >> j;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetBytesProcessed(static_cast<int64_t>(state.iterations() * input.size()));
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(ParseStreamValues, ndjson, false);
|
||||||
|
BENCHMARK_CAPTURE(ParseStreamValues, numbers, true);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
// parse JSON from string
|
// parse JSON from string
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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
|
||||||
|
|
||||||
|
#include "doctest_compatibility.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Parsing from a FILE* locks the file once and reads it with the unlocked
|
||||||
|
// stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that
|
||||||
|
// this is not observable: the results are the same, the file is unlocked
|
||||||
|
// again when parsing ends - also with an exception - and the same thread can
|
||||||
|
// keep using the file while it is locked, e.g., from a SAX callback.
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// a file name of its own for every run: the test runs for several C++
|
||||||
|
// standards and configurations at the same time, in the same directory
|
||||||
|
std::string temp_file_name()
|
||||||
|
{
|
||||||
|
std::random_device rd;
|
||||||
|
return "unit-file_input-" + std::to_string(rd()) + ".tmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
using file_ptr = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;
|
||||||
|
|
||||||
|
// write @a content to the file @a name and open it for reading
|
||||||
|
file_ptr open_with(const char* name, const std::string& content)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::ofstream file(name, std::ios::binary);
|
||||||
|
file << content;
|
||||||
|
}
|
||||||
|
return {std::fopen(name, "rb"), &std::fclose};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stdio lock is recursive, so whether parsing released it can only be
|
||||||
|
// seen from another thread: ftrylockfile() fails there while this thread
|
||||||
|
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
|
||||||
|
#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
|
||||||
|
void check_unlocked(std::FILE* f)
|
||||||
|
{
|
||||||
|
int result = -1;
|
||||||
|
std::thread other([f, &result]
|
||||||
|
{
|
||||||
|
result = ftrylockfile(f);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
funlockfile(f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
other.join();
|
||||||
|
CHECK(result == 0);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void check_unlocked(std::FILE* /*f*/) {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// SAX consumer that also reads the position of the file it is parsing, which
|
||||||
|
// takes the file's lock again on the same thread
|
||||||
|
class position_reading_sax : public nlohmann::json_sax<json>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit position_reading_sax(std::FILE* f) : m_file(f) {}
|
||||||
|
position_reading_sax(const position_reading_sax&) = delete;
|
||||||
|
position_reading_sax(position_reading_sax&&) = delete;
|
||||||
|
position_reading_sax& operator=(const position_reading_sax&) = delete;
|
||||||
|
position_reading_sax& operator=(position_reading_sax&&) = delete;
|
||||||
|
~position_reading_sax() override = default;
|
||||||
|
|
||||||
|
bool null() override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool boolean(bool /*val*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool number_integer(number_integer_t /*val*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool number_unsigned(number_unsigned_t /*val*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool string(string_t& /*val*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool binary(binary_t& /*val*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool start_object(std::size_t /*elements*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool key(string_t& /*val*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool end_object() override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool start_array(std::size_t /*elements*/) override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool end_array() override
|
||||||
|
{
|
||||||
|
return record();
|
||||||
|
}
|
||||||
|
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<long>& positions() const
|
||||||
|
{
|
||||||
|
return m_positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool record()
|
||||||
|
{
|
||||||
|
m_positions.push_back(std::ftell(m_file));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::FILE* m_file;
|
||||||
|
std::vector<long> m_positions{}; // NOLINT(readability-redundant-member-init)
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("parsing from a FILE*")
|
||||||
|
{
|
||||||
|
const std::string temp_name = temp_file_name();
|
||||||
|
const char* const name = temp_name.c_str();
|
||||||
|
const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}";
|
||||||
|
|
||||||
|
SECTION("values are the same as when parsing from a string")
|
||||||
|
{
|
||||||
|
const file_ptr f = open_with(name, input);
|
||||||
|
REQUIRE(f != nullptr);
|
||||||
|
CHECK(json::parse(f.get()) == json::parse(input));
|
||||||
|
// the whole input was consumed
|
||||||
|
std::array<char, 8> rest{};
|
||||||
|
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) == nullptr);
|
||||||
|
check_unlocked(f.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(JSON_NOEXCEPTION)
|
||||||
|
SECTION("the file is unlocked after a parse error")
|
||||||
|
{
|
||||||
|
const file_ptr f = open_with(name, "[1, 2, x]");
|
||||||
|
REQUIRE(f != nullptr);
|
||||||
|
json _;
|
||||||
|
CHECK_THROWS_WITH_AS(_ = json::parse(f.get()),
|
||||||
|
"[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'",
|
||||||
|
json::parse_error&);
|
||||||
|
check_unlocked(f.get());
|
||||||
|
|
||||||
|
// the file is still usable: the rest of the input is there
|
||||||
|
std::array<char, 8> rest{};
|
||||||
|
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
|
||||||
|
CHECK(std::string(rest.data()) == "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !defined(JSON_NOEXCEPTION)
|
||||||
|
|
||||||
|
SECTION("the file can be parsed again and read with stdio afterwards")
|
||||||
|
{
|
||||||
|
const file_ptr f = open_with(name, "[1] [2] tail");
|
||||||
|
REQUIRE(f != nullptr);
|
||||||
|
|
||||||
|
// sax_parse with strict == false stops after the first value; an array
|
||||||
|
// ends with its closing bracket, so the space after it is not read
|
||||||
|
json j;
|
||||||
|
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax1(j);
|
||||||
|
CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false));
|
||||||
|
CHECK(j == json({1}));
|
||||||
|
check_unlocked(f.get());
|
||||||
|
|
||||||
|
json k;
|
||||||
|
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax2(k);
|
||||||
|
CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false));
|
||||||
|
CHECK(k == json({2}));
|
||||||
|
check_unlocked(f.get());
|
||||||
|
|
||||||
|
std::array<char, 8> rest{};
|
||||||
|
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
|
||||||
|
CHECK(std::string(rest.data()) == " tail");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("the same thread can use the file while it is parsed")
|
||||||
|
{
|
||||||
|
const file_ptr f = open_with(name, "[10, 20, 30]");
|
||||||
|
REQUIRE(f != nullptr);
|
||||||
|
position_reading_sax sax(f.get());
|
||||||
|
CHECK(json::sax_parse(f.get(), &sax));
|
||||||
|
// start_array, three numbers, end_array
|
||||||
|
CHECK(sax.positions().size() == 5);
|
||||||
|
CHECK(sax.positions().front() == 1);
|
||||||
|
check_unlocked(f.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("binary formats read from a FILE*")
|
||||||
|
{
|
||||||
|
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
|
||||||
|
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
|
||||||
|
const file_ptr f = open_with(name, std::string(reinterpret_cast<const char*>(cbor.data()), cbor.size()));
|
||||||
|
REQUIRE(f != nullptr);
|
||||||
|
CHECK(json::from_cbor(f.get()) == j);
|
||||||
|
check_unlocked(f.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
static_cast<void>(std::remove(name));
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <streambuf>
|
#include <streambuf>
|
||||||
@@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf
|
|||||||
std::size_t m_pos = 0;
|
std::size_t m_pos = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A streambuf that serves its input in get areas of a fixed size, so that the
|
||||||
|
// end of a number falls on every possible offset of a get area. The lexer scans
|
||||||
|
// the get area directly; the stream position must not depend on its size.
|
||||||
|
class chunked_streambuf : public std::streambuf
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
chunked_streambuf(std::string s, std::size_t chunk_size)
|
||||||
|
: m_data(std::move(s)), m_chunk_size(chunk_size)
|
||||||
|
{}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int_type underflow() override
|
||||||
|
{
|
||||||
|
if (gptr() < egptr())
|
||||||
|
{
|
||||||
|
return traits_type::to_int_type(*gptr());
|
||||||
|
}
|
||||||
|
m_pos += static_cast<std::size_t>(egptr() - eback());
|
||||||
|
if (m_pos >= m_data.size())
|
||||||
|
{
|
||||||
|
setg(nullptr, nullptr, nullptr);
|
||||||
|
return traits_type::eof();
|
||||||
|
}
|
||||||
|
char* const begin = &m_data[m_pos];
|
||||||
|
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
|
||||||
|
return traits_type::to_int_type(*gptr());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_data;
|
||||||
|
std::size_t m_pos = 0;
|
||||||
|
std::size_t m_chunk_size;
|
||||||
|
};
|
||||||
|
|
||||||
// read the characters that are left in a stream
|
// read the characters that are left in a stream
|
||||||
std::string remaining(std::istream& is)
|
std::string remaining(std::istream& is)
|
||||||
{
|
{
|
||||||
@@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
|
|||||||
json _;
|
json _;
|
||||||
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
|
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("the position does not depend on the get area of the streambuf")
|
||||||
|
{
|
||||||
|
const std::vector<std::size_t> chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096};
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes)
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size);
|
||||||
|
std::istream input(&buf);
|
||||||
|
json j;
|
||||||
|
|
||||||
|
input >> j;
|
||||||
|
CHECK(j == 12345678901234567890u);
|
||||||
|
input >> j;
|
||||||
|
CHECK(j == true);
|
||||||
|
input >> j;
|
||||||
|
CHECK(j == "a long string value");
|
||||||
|
input >> j;
|
||||||
|
CHECK(j == -1250.0);
|
||||||
|
CHECK(remaining(input) == ",[1,2]42");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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}};
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | 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
|
||||||
|
|
||||||
|
#include "doctest_compatibility.h"
|
||||||
|
|
||||||
|
// the library undefines its configuration macros at the end of the header
|
||||||
|
#if defined(JSON_DISABLE_STREAM_BULK_SCAN) && JSON_DISABLE_STREAM_BULK_SCAN
|
||||||
|
#define TEST_STREAM_BULK_SCAN 0
|
||||||
|
#else
|
||||||
|
#define TEST_STREAM_BULK_SCAN 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
using nlohmann::json;
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <istream>
|
||||||
|
#include <random>
|
||||||
|
#include <sstream>
|
||||||
|
#include <streambuf>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// The lexer scans the characters in the get area of a std::istream's
|
||||||
|
// streambuf directly instead of reading them one at a time. These tests pin
|
||||||
|
// that this is not observable: for any get area size - including a streambuf
|
||||||
|
// without a get area, which still takes the character-at-a-time path - the
|
||||||
|
// parsed values, the exception messages (including the "last read" token),
|
||||||
|
// the stream state, and the characters left in the stream are the same. See
|
||||||
|
// unit-stream-bulk-scan-disabled.cpp for JSON_DISABLE_STREAM_BULK_SCAN.
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// A streambuf that serves its input in get areas of a fixed size, so that
|
||||||
|
// tokens, numbers, UTF-8 sequences, and indentation cross the end of the get
|
||||||
|
// area at every possible offset. With a size of 0, it keeps no get area at all
|
||||||
|
// and serves every character from underflow()/uflow().
|
||||||
|
class chunked_streambuf : public std::streambuf
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
chunked_streambuf(std::string s, std::size_t chunk_size)
|
||||||
|
: m_data(std::move(s)), m_chunk_size(chunk_size)
|
||||||
|
{}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int_type underflow() override
|
||||||
|
{
|
||||||
|
if (m_chunk_size == 0)
|
||||||
|
{
|
||||||
|
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof();
|
||||||
|
}
|
||||||
|
if (gptr() < egptr())
|
||||||
|
{
|
||||||
|
return traits_type::to_int_type(*gptr());
|
||||||
|
}
|
||||||
|
// the previous get area has been consumed
|
||||||
|
m_pos += static_cast<std::size_t>(egptr() - eback());
|
||||||
|
if (m_pos >= m_data.size())
|
||||||
|
{
|
||||||
|
setg(nullptr, nullptr, nullptr);
|
||||||
|
return traits_type::eof();
|
||||||
|
}
|
||||||
|
char* const begin = &m_data[m_pos];
|
||||||
|
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
|
||||||
|
return traits_type::to_int_type(*gptr());
|
||||||
|
}
|
||||||
|
|
||||||
|
int_type uflow() override
|
||||||
|
{
|
||||||
|
if (m_chunk_size == 0)
|
||||||
|
{
|
||||||
|
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof();
|
||||||
|
}
|
||||||
|
return std::streambuf::uflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_data;
|
||||||
|
std::size_t m_pos = 0;
|
||||||
|
std::size_t m_chunk_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
// how a test reads from the stream
|
||||||
|
enum class read_mode
|
||||||
|
{
|
||||||
|
extract, // up to four times operator>>, as for concatenated values
|
||||||
|
parse, // json::parse(stream)
|
||||||
|
parse_lenient, // json::parse(stream) with comments
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read @a input from a chunked_streambuf and describe everything observable:
|
||||||
|
// the values or the exception, the stream state after each value, and the
|
||||||
|
// characters left in the stream.
|
||||||
|
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode)
|
||||||
|
{
|
||||||
|
chunked_streambuf buf(input, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
std::string result;
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
json j;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (mode == read_mode::extract)
|
||||||
|
{
|
||||||
|
is >> j;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
j = json::parse(is, nullptr, true, mode == read_mode::parse_lenient);
|
||||||
|
}
|
||||||
|
result += "value " + j.dump() + "; state " + std::to_string(static_cast<unsigned int>(is.rdstate())) + "\n";
|
||||||
|
}
|
||||||
|
catch (const json::exception& e)
|
||||||
|
{
|
||||||
|
result += std::string("exception ") + e.what() + "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mode != read_mode::extract)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
is.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
is.clear();
|
||||||
|
result += "remaining ";
|
||||||
|
char c = 0;
|
||||||
|
while (is.get(c))
|
||||||
|
{
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get area sizes that put every boundary everywhere in short inputs, plus one
|
||||||
|
// that holds most inputs completely
|
||||||
|
std::vector<std::size_t> chunk_sizes()
|
||||||
|
{
|
||||||
|
return {1, 2, 3, 4, 5, 7, 8, 9, 13, 16, 64, 4096};
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid and invalid inputs covering every token type and every bulk path:
|
||||||
|
// strings (plain runs, escapes, UTF-8), numbers (all grammar parts, too large
|
||||||
|
// for an integer), and indentation
|
||||||
|
std::vector<std::string> corpus()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
{
|
||||||
|
// numbers, terminated in every way
|
||||||
|
"0", "-0", "123", "-123", "1.5", "-1.5e+10", "2E-3", "123456789012345678901234567890",
|
||||||
|
"18446744073709551615", "18446744073709551616", "-9223372036854775809",
|
||||||
|
"123 456", "123,456", "123]", "1true", "42\nfoo", "1.25e3}", "7\t8\r9",
|
||||||
|
// malformed numbers
|
||||||
|
"1.", "-", "1e", "1e+", "01", "-a", "1.x", "1ee5",
|
||||||
|
// strings
|
||||||
|
R"("")", R"("abc")", R"("a long string with a run of ordinary characters to scan in bulk")",
|
||||||
|
R"("escapes \" \\ \/ \b \f \n \r \t é 😀")",
|
||||||
|
"\"UTF-8: \xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80 done\"",
|
||||||
|
// malformed strings
|
||||||
|
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
|
||||||
|
R"("\ud800 lone surrogate")",
|
||||||
|
// literals
|
||||||
|
"true", "false", "null", "tru", "nul", "falsey",
|
||||||
|
// structures
|
||||||
|
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
|
||||||
|
// pretty-printed
|
||||||
|
"{\n \"key\": [\n 1,\n -2.5,\n \"three\"\n ]\n}",
|
||||||
|
"[\n \"deep\"\n ]",
|
||||||
|
"{\n \"a\": 1,\n \"b\": 2x\n}",
|
||||||
|
// concatenated values and whitespace
|
||||||
|
"1 2 3", R"("a""b")", "[1][2]", "{}{}", " \n\t 42 \n\t ", "",
|
||||||
|
// comments (only accepted by parse_lenient)
|
||||||
|
"/* comment */ 1", "// comment\n[1, /* c */ 2]",
|
||||||
|
// byte order mark
|
||||||
|
"\xef\xbb\xbf[1]",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("parsing from a std::istream scans the get area in bulk")
|
||||||
|
{
|
||||||
|
#if !defined(JSON_NOEXCEPTION)
|
||||||
|
SECTION("results do not depend on the get area")
|
||||||
|
{
|
||||||
|
const std::vector<read_mode> read_modes = {read_mode::extract, read_mode::parse, read_mode::parse_lenient};
|
||||||
|
for (const auto& input : corpus())
|
||||||
|
{
|
||||||
|
for (const read_mode mode : read_modes)
|
||||||
|
{
|
||||||
|
// no get area: the character-at-a-time path
|
||||||
|
const std::string expected = observe(input, 0, mode);
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(input)
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
CAPTURE(static_cast<int>(mode))
|
||||||
|
CHECK(observe(input, chunk_size, mode) == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !defined(JSON_NOEXCEPTION)
|
||||||
|
|
||||||
|
SECTION("values are the same as when parsing from a string")
|
||||||
|
{
|
||||||
|
const std::string input = R"({"name": "nlohmann::json", "numbers": [1, -2, 3.5, 4e10, 18446744073709551615],)"
|
||||||
|
"\n \"text\": \"caf\xc3\xa9 \\u00e9 \\\"quoted\\\"\", \"nested\": [[{}], [[]]], \"flags\": [true, false, null]}";
|
||||||
|
const json expected = json::parse(input);
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(input, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
CHECK(json::parse(is) == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::istringstream ss(input);
|
||||||
|
CHECK(json::parse(ss) == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(JSON_NOEXCEPTION)
|
||||||
|
SECTION("a token that spans several get areas is reported completely")
|
||||||
|
{
|
||||||
|
// the error is found after the string has crossed many get areas; the
|
||||||
|
// token string holds everything since the start of the last token
|
||||||
|
const std::string input = R"(["this string is longer than every get area", tru])";
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(input, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
CHECK_THROWS_WITH_AS(is >> j,
|
||||||
|
"[json.exception.parse_error.101] parse error at line 1, column 50: syntax error while parsing value - invalid literal; last read: '\"this string is longer than every get area\", tru]'",
|
||||||
|
json::parse_error&);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string unterminated = R"("this string is longer than every get area)";
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(unterminated, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
CHECK_THROWS_WITH_AS(is >> j,
|
||||||
|
"[json.exception.parse_error.101] parse error at line 1, column 43: syntax error while parsing value - invalid string: missing closing quote; last read: '\"this string is longer than every get area'",
|
||||||
|
json::parse_error&);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error before the first token reports everything read")
|
||||||
|
{
|
||||||
|
chunked_streambuf buf(" \n x", 2);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
CHECK_THROWS_WITH_AS(is >> j,
|
||||||
|
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - invalid literal; last read: ' <U+000A> x'",
|
||||||
|
json::parse_error&);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("lines and columns are counted across get areas")
|
||||||
|
{
|
||||||
|
const std::string input = "[\n 1,\n \"two\",\n 3.0,\n x\n]";
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(input, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
CHECK_THROWS_WITH_AS(is >> j,
|
||||||
|
"[json.exception.parse_error.101] parse error at line 5, column 5: syntax error while parsing value - invalid literal; last read: '3.0,<U+000A> x'",
|
||||||
|
json::parse_error&);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !defined(JSON_NOEXCEPTION)
|
||||||
|
|
||||||
|
SECTION("many values from one stream")
|
||||||
|
{
|
||||||
|
std::string input;
|
||||||
|
for (int i = 0; i < 200; ++i)
|
||||||
|
{
|
||||||
|
input += R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","score":)" + std::to_string(i) + ".5}\n";
|
||||||
|
input += std::to_string(i * 7919) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(input, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
int count = 0;
|
||||||
|
while (!(is >> std::ws).eof())
|
||||||
|
{
|
||||||
|
is >> j;
|
||||||
|
if (count % 2 == 0)
|
||||||
|
{
|
||||||
|
CHECK(j["id"] == count / 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CHECK(j == (count / 2) * 7919);
|
||||||
|
}
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
CHECK(count == 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("the stream can be used after parsing")
|
||||||
|
{
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(R"({"a": "some text"} trailing words 42)", chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
is >> j;
|
||||||
|
CHECK(j == json({{"a", "some text"}}));
|
||||||
|
|
||||||
|
std::string word;
|
||||||
|
int number = 0;
|
||||||
|
is >> word;
|
||||||
|
CHECK(word == "trailing");
|
||||||
|
is >> word >> number;
|
||||||
|
CHECK(word == "words");
|
||||||
|
CHECK(number == 42);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("std::stringstream and std::ifstream")
|
||||||
|
{
|
||||||
|
const std::string input = R"([1, 2.5, "three", {"four": [true, null]}] 5)";
|
||||||
|
|
||||||
|
std::stringstream ss(input);
|
||||||
|
json j1;
|
||||||
|
json j2;
|
||||||
|
ss >> j1 >> j2;
|
||||||
|
CHECK(j1 == json::parse(R"([1, 2.5, "three", {"four": [true, null]}])"));
|
||||||
|
CHECK(j2 == 5);
|
||||||
|
CHECK(ss.eof());
|
||||||
|
|
||||||
|
// a file name of its own for every run: the test runs for several C++
|
||||||
|
// standards and configurations at the same time, in the same directory
|
||||||
|
std::random_device rd;
|
||||||
|
const std::string temp_name = "unit-stream-bulk-scan-" + std::to_string(rd()) + ".tmp";
|
||||||
|
const char* const filename = temp_name.c_str();
|
||||||
|
{
|
||||||
|
std::ofstream file(filename);
|
||||||
|
file << input;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::ifstream file(filename);
|
||||||
|
json k1;
|
||||||
|
json k2;
|
||||||
|
file >> k1 >> k2;
|
||||||
|
CHECK(k1 == j1);
|
||||||
|
CHECK(k2 == j2);
|
||||||
|
}
|
||||||
|
static_cast<void>(std::remove(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("binary formats read from a stream")
|
||||||
|
{
|
||||||
|
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
|
||||||
|
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
|
||||||
|
const std::string bytes(reinterpret_cast<const char*>(cbor.data()), cbor.size());
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(bytes, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
CHECK(json::from_cbor(is) == j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TEST_STREAM_BULK_SCAN
|
||||||
|
SECTION("the adapter keeps no token unless the lexer starts one")
|
||||||
|
{
|
||||||
|
// the binary readers read through the same adapter, but never start a
|
||||||
|
// token; refilling the get area must then not collect the input
|
||||||
|
chunked_streambuf buf(std::string(1000, 'x'), 7);
|
||||||
|
std::istream is(&buf);
|
||||||
|
nlohmann::detail::input_stream_adapter ia(is);
|
||||||
|
for (int i = 0; i < 1000; ++i)
|
||||||
|
{
|
||||||
|
CHECK(ia.get_character() == 'x');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> token;
|
||||||
|
ia.copy_token(token, false);
|
||||||
|
CHECK(token.empty());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SECTION("SAX parsing from a stream")
|
||||||
|
{
|
||||||
|
const std::string input = R"({"a": [1, "two", 3.5]} rest)";
|
||||||
|
for (const std::size_t chunk_size : chunk_sizes())
|
||||||
|
{
|
||||||
|
CAPTURE(chunk_size)
|
||||||
|
chunked_streambuf buf(input, chunk_size);
|
||||||
|
std::istream is(&buf);
|
||||||
|
json j;
|
||||||
|
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(j);
|
||||||
|
CHECK(json::sax_parse(is, &sax, json::input_format_t::json, false));
|
||||||
|
CHECK(j == json::parse(R"({"a": [1, "two", 3.5]})"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user