Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
598fbd0417 | ||
|
|
898084b386 | ||
|
|
f13fbd2577 | ||
|
|
c391abccb1 | ||
|
|
d5631dccbd | ||
|
|
80d00eda50 |
No files matched your search
@@ -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
|
||||
- [`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
|
||||
- [`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
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
- 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"
|
||||
|
||||
|
||||
@@ -68,14 +68,6 @@ basic_json(basic_json&& other) noexcept;
|
||||
`std::unordered_map`, `std::multimap`, and `std::unordered_multimap` with a `key_type` compatible to `string_t`
|
||||
and a `value_type` from which a `basic_json` value can be constructed.
|
||||
- **strings**: `string_t`, string literals, and all compatible string containers can be used.
|
||||
|
||||
!!! note "Rvalue containers are moved from"
|
||||
|
||||
If `val` is an rvalue of a compatible array-like or object-like container other than `array_t`/`object_t`
|
||||
(e.g., `json j = std::move(vec);` or `json j(std::vector<std::string>{...});`), the elements are moved into
|
||||
the JSON value instead of copied. Afterwards, the elements of the source container are in a valid but
|
||||
unspecified state (**moved-from**). Keys of associative containers are `const` and therefore still copied.
|
||||
Lvalues and `const` rvalues are copied, and the source is left unchanged.
|
||||
- **numbers**: [`number_integer_t`](number_integer_t.md), [`number_unsigned_t`](number_unsigned_t.md),
|
||||
[`number_float_t`](number_float_t.md), and all convertible number types such as `int`, `size_t`, `int64_t`, `float`
|
||||
or `double` can be used.
|
||||
@@ -244,9 +236,7 @@ basic_json(basic_json&& other) noexcept;
|
||||
2. No-throw guarantee: this constructor never throws exceptions.
|
||||
3. Depends on the called constructor. For types directly supported by the library (i.e., all types for which no
|
||||
`to_json()` function was provided), a strong guarantee holds: if an exception is thrown, there are no changes to any
|
||||
JSON value. **Exception:** when `val` is an rvalue array-like or object-like container, its elements are moved one
|
||||
by one; if an exception is thrown in the middle of the conversion (e.g., an allocation failure), the JSON value is
|
||||
unchanged, but the source container may be left partially moved-from.
|
||||
JSON value.
|
||||
4. Depends on the called constructor. For types directly supported by the library (i.e., all types for which no
|
||||
`to_json()` function was provided), a strong guarantee holds: if an exception is thrown, there are no changes to any
|
||||
JSON value.
|
||||
|
||||
@@ -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
|
||||
[`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
|
||||
|
||||
??? 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
|
||||
- [`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
|
||||
- [`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
|
||||
|
||||
@@ -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.
|
||||
- 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.
|
||||
- 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"
|
||||
|
||||
|
||||
@@ -116,6 +116,10 @@ consumer `sax` has a super-linear complexity.
|
||||
|
||||
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
|
||||
|
||||
??? example
|
||||
@@ -137,6 +141,10 @@ A UTF-8 byte order mark is silently ignored.
|
||||
|
||||
- [parse](parse.md) - deserialize from a compatible input
|
||||
- [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
|
||||
|
||||
@@ -148,6 +156,10 @@ A UTF-8 byte order mark is silently ignored.
|
||||
- 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
|
||||
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"
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ header. See also the [macro overview page](../../features/macros.md).
|
||||
|
||||
## 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
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# 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.
|
||||
|
||||
!!! warning "Define consistently"
|
||||
|
||||
The macro selects between two definitions of the library's internal stream input adapter. It must therefore be
|
||||
defined identically for **every** translation unit that includes the library; mixing translation units that define
|
||||
it with ones that do not is an ODR violation. Prefer setting it as a compile definition on the target rather than
|
||||
with `#!cpp #define` in individual source files.
|
||||
|
||||
!!! 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,113 @@
|
||||
# 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.
|
||||
|
||||
!!! warning "Define consistently"
|
||||
|
||||
The macro selects between two definitions of the library's internal `FILE*` input adapter. It must therefore be
|
||||
defined identically for **every** translation unit that includes the library; mixing translation units that define
|
||||
it with ones that do not is an ODR violation (and can leave a file locked). Prefer setting it as a compile
|
||||
definition on the target rather than with `#!cpp #define` in individual source files.
|
||||
|
||||
!!! 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
|
||||
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"
|
||||
|
||||
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
|
||||
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
|
||||
diagnostic is preserved). Streaming inputs (files, `std::istream`, wide strings, user-defined adapters) always use the
|
||||
scalar path.
|
||||
diagnostic is preserved). A `std::istream` uses simdutf for the characters its stream buffer holds (see
|
||||
[`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
|
||||
simdutf library. When it is not defined, no simdutf header is included and there is no dependency.
|
||||
|
||||
@@ -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
|
||||
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_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) - read the stream one character at a time
|
||||
|
||||
## Version history
|
||||
|
||||
@@ -133,3 +134,6 @@ being read.
|
||||
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
|
||||
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).
|
||||
@@ -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).
|
||||
|
||||
## `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`
|
||||
|
||||
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).
|
||||
|
||||
## `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`
|
||||
|
||||
When defined, `<nlohmann/json.hpp>` does not include `<nlohmann/json_literals.hpp>` with the user-defined string literals
|
||||
|
||||
@@ -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
|
||||
[`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
|
||||
|
||||
The library offers two parsing models:
|
||||
|
||||
@@ -9,15 +9,6 @@ published on the [GitHub releases page](https://github.com/nlohmann/json/release
|
||||
This documentation is built from the `develop` branch and may describe changes that are not part of a release
|
||||
yet. Their version numbers are followed by an <span class="unreleased-version">unreleased</span> badge.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Converting an rvalue container to JSON (`json j = std::move(vec);`, `json j = std::map<std::string, std::string>{...};`)
|
||||
now **moves** the elements instead of copying them (e.g., `std::vector<std::string>` with 100,000 strings: 4.7 ms to
|
||||
2.3 ms). Two consequences: the source container is left in a valid but unspecified (moved-from) state, and if an
|
||||
exception is thrown during the conversion, the source may be partially moved-from, so the strong exception guarantee
|
||||
no longer holds for the source of an rvalue conversion (the JSON value itself is unaffected). Lvalues and `const`
|
||||
objects are still copied. See [`basic_json`](../api/basic_json/basic_json.md).
|
||||
|
||||
## v3.12.0 (2025-04-11)
|
||||
|
||||
Fixes bugs found in 3.11.3 and adds several features. All changes are backward-compatible.
|
||||
|
||||
@@ -297,7 +297,9 @@ nav:
|
||||
- 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md
|
||||
- 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.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_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_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md
|
||||
- 'JSON_HAS_RANGE_VIEW_CONVERSION': api/macros/json_has_range_view_conversion.md
|
||||
|
||||
@@ -266,22 +266,6 @@ template<typename ConstructibleArrayType>
|
||||
void from_json_array_reserve(ConstructibleArrayType& /*arr*/, std::size_t /*size*/, priority_tag<0> /*unused*/)
|
||||
{}
|
||||
|
||||
// append through push_back() where available; containers without it (such as
|
||||
// std::set) fall back to insert(end(), ...), which is what std::inserter did
|
||||
template<typename ConstructibleArrayType, typename ValueType>
|
||||
auto from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<1> /*unused*/)
|
||||
-> decltype(arr.push_back(std::forward<ValueType>(value)), void())
|
||||
{
|
||||
arr.push_back(std::forward<ValueType>(value));
|
||||
}
|
||||
|
||||
template<typename ConstructibleArrayType, typename ValueType>
|
||||
void from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<0> /*unused*/)
|
||||
{
|
||||
using std::end;
|
||||
arr.insert(end(arr), std::forward<ValueType>(value));
|
||||
}
|
||||
|
||||
template<typename BasicJsonType, typename ConstructibleArrayType,
|
||||
enable_if_t<
|
||||
std::is_assignable<ConstructibleArrayType&, ConstructibleArrayType>::value,
|
||||
@@ -291,15 +275,17 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p
|
||||
j.template get<typename ConstructibleArrayType::value_type>(),
|
||||
void())
|
||||
{
|
||||
using std::end;
|
||||
|
||||
ConstructibleArrayType ret;
|
||||
const auto* inner_array = j.template get_ptr<const typename BasicJsonType::array_t*>();
|
||||
from_json_array_reserve(ret, inner_array->size(), priority_tag<1> {});
|
||||
for (const auto& elem : *inner_array)
|
||||
from_json_array_reserve(ret, j.size(), priority_tag<1> {});
|
||||
std::transform(j.begin(), j.end(),
|
||||
std::inserter(ret, end(ret)), [](const BasicJsonType & i)
|
||||
{
|
||||
// get<BasicJsonType>() returns *this, this won't call a from_json
|
||||
// method when value_type is BasicJsonType
|
||||
from_json_array_append(ret, elem.template get<typename ConstructibleArrayType::value_type>(), priority_tag<1> {});
|
||||
}
|
||||
return i.template get<typename ConstructibleArrayType::value_type>();
|
||||
});
|
||||
arr = std::move(ret);
|
||||
}
|
||||
|
||||
@@ -397,13 +383,9 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj)
|
||||
ConstructibleObjectType ret;
|
||||
const auto* inner_object = j.template get_ptr<const typename BasicJsonType::object_t*>();
|
||||
from_json_object_reserve(ret, inner_object->size(), priority_tag<1> {});
|
||||
using value_type = typename ConstructibleObjectType::value_type;
|
||||
for (const auto& p : *inner_object)
|
||||
{
|
||||
// the keys of the source are sorted for the usual object types, so
|
||||
// hinting the end makes every insertion constant-time (for containers
|
||||
// that order their keys the same way)
|
||||
ret.insert(ret.end(), value_type(p.first, p.second.template get<typename ConstructibleObjectType::mapped_type>()));
|
||||
ret.emplace(p.first, p.second.template get<typename ConstructibleObjectType::mapped_type>());
|
||||
}
|
||||
obj = std::move(ret);
|
||||
}
|
||||
|
||||
@@ -167,17 +167,6 @@ struct external_constructor<value_t::array>
|
||||
j.replace_value(value_t::array, value);
|
||||
}
|
||||
|
||||
/// an rvalue container: move its elements
|
||||
template<typename BasicJsonType, typename CompatibleArrayType>
|
||||
static void construct_moving(BasicJsonType& j, CompatibleArrayType& arr)
|
||||
{
|
||||
using std::begin;
|
||||
using std::end;
|
||||
|
||||
const typename BasicJsonType::json_value value(j.template create<typename BasicJsonType::array_t>(std::make_move_iterator(begin(arr)), std::make_move_iterator(end(arr))));
|
||||
j.replace_value(value_t::array, value);
|
||||
}
|
||||
|
||||
template<typename BasicJsonType>
|
||||
static void construct(BasicJsonType& j, const std::vector<bool>& arr)
|
||||
{
|
||||
@@ -228,17 +217,6 @@ struct external_constructor<value_t::object>
|
||||
j.replace_value(value_t::object, value);
|
||||
}
|
||||
|
||||
/// an rvalue container: move its values
|
||||
template<typename BasicJsonType, typename CompatibleObjectType>
|
||||
static void construct_moving(BasicJsonType& j, CompatibleObjectType& obj)
|
||||
{
|
||||
using std::begin;
|
||||
using std::end;
|
||||
|
||||
const typename BasicJsonType::json_value value(j.template create<typename BasicJsonType::object_t>(std::make_move_iterator(begin(obj)), std::make_move_iterator(end(obj))));
|
||||
j.replace_value(value_t::object, value);
|
||||
}
|
||||
|
||||
template < typename BasicJsonType, typename CompatibleObjectType,
|
||||
enable_if_t < !std::is_same<CompatibleObjectType, typename BasicJsonType::object_t>::value, int > = 0 >
|
||||
static void construct(BasicJsonType& j, const CompatibleObjectType& obj)
|
||||
@@ -376,66 +354,6 @@ inline void to_json(BasicJsonType& j, const CompatibleArrayType& arr)
|
||||
external_constructor<value_t::array>::construct(j, arr);
|
||||
}
|
||||
|
||||
// types that have a dedicated to_json overload and must not be treated as generic containers
|
||||
template<typename T>
|
||||
struct has_dedicated_to_json : std::false_type {};
|
||||
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
template<>
|
||||
struct has_dedicated_to_json<std_fs::path> : std::true_type {};
|
||||
#endif
|
||||
|
||||
#if defined(__cpp_lib_char8_t)
|
||||
template<typename Tr, typename Allocator>
|
||||
struct has_dedicated_to_json<std::basic_string<char8_t, Tr, Allocator>> : std::true_type {};
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION
|
||||
template<typename T>
|
||||
struct is_range_view_type : is_compatible_range_view<T> {};
|
||||
#else
|
||||
template<typename T>
|
||||
struct is_range_view_type : std::false_type {};
|
||||
#endif
|
||||
|
||||
#if JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS
|
||||
template<typename T>
|
||||
struct is_enum_keyed_map_type : is_enum_keyed_map<T> {};
|
||||
#else
|
||||
template<typename T>
|
||||
struct is_enum_keyed_map_type : std::false_type {};
|
||||
#endif
|
||||
|
||||
// rvalue (non-const) container: the generic to_json takes its elements by move
|
||||
template<typename T>
|
||||
using is_movable_container_rvalue = conjunction <
|
||||
negation<std::is_lvalue_reference<T>>,
|
||||
negation<std::is_const<T>>,
|
||||
negation<has_dedicated_to_json<T>>,
|
||||
negation<is_basic_json<T>>,
|
||||
negation<is_range_view_type<T>>,
|
||||
negation<is_enum_keyed_map_type<T>>,
|
||||
negation<std::is_same<T, std::vector<bool>>> >;
|
||||
|
||||
// rvalue array-like containers
|
||||
template<typename BasicJsonType, typename T>
|
||||
using is_rvalue_array_to_json = conjunction <
|
||||
is_movable_container_rvalue<T>,
|
||||
negation<std::is_same<T, typename BasicJsonType::array_t>>,
|
||||
negation<std::is_same<T, typename BasicJsonType::binary_t>>,
|
||||
is_compatible_array_type<BasicJsonType, T>,
|
||||
negation<is_compatible_object_type<BasicJsonType, T>>,
|
||||
negation<is_compatible_string_type<BasicJsonType, T>>,
|
||||
negation<is_compatible_binary_type<BasicJsonType, T>> >;
|
||||
|
||||
// rvalue containers: their elements are moved instead of copied
|
||||
template < typename BasicJsonType, typename CompatibleArrayType,
|
||||
enable_if_t < is_rvalue_array_to_json<BasicJsonType, CompatibleArrayType>::value, int > = 0 >
|
||||
inline void to_json(BasicJsonType& j, CompatibleArrayType && arr)
|
||||
{
|
||||
external_constructor<value_t::array>::construct_moving(j, arr);
|
||||
}
|
||||
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION
|
||||
template < typename BasicJsonType, typename T,
|
||||
enable_if_t < is_compatible_range_view<std::remove_cvref_t<T>>::value
|
||||
@@ -509,20 +427,6 @@ inline void to_json(BasicJsonType& j, const EnumKeyedMap& map)
|
||||
}
|
||||
#endif
|
||||
|
||||
// rvalue object-like containers
|
||||
template<typename BasicJsonType, typename T>
|
||||
using is_rvalue_object_to_json = conjunction <
|
||||
is_movable_container_rvalue<T>,
|
||||
negation<std::is_same<T, typename BasicJsonType::object_t>>,
|
||||
is_compatible_object_type<BasicJsonType, T> >;
|
||||
|
||||
template < typename BasicJsonType, typename CompatibleObjectType,
|
||||
enable_if_t < is_rvalue_object_to_json<BasicJsonType, CompatibleObjectType>::value, int > = 0 >
|
||||
inline void to_json(BasicJsonType& j, CompatibleObjectType && obj)
|
||||
{
|
||||
external_constructor<value_t::object>::construct_moving(j, obj);
|
||||
}
|
||||
|
||||
template<typename BasicJsonType>
|
||||
inline void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj)
|
||||
{
|
||||
|
||||
@@ -428,7 +428,7 @@ void templated_json_throw(ExceptionType exception)
|
||||
@throw type_error.302 always
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const char* expected, const BasicJsonType& j)
|
||||
JSON_HEDLEY_NO_RETURN inline void throw_type_must_be(const char* expected, const BasicJsonType& j)
|
||||
{
|
||||
static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined
|
||||
static_cast<void>(j);
|
||||
@@ -443,7 +443,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha
|
||||
@throw type_error always
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_use_with(const exception_id id_, const char* operation, const BasicJsonType& j)
|
||||
JSON_HEDLEY_NO_RETURN inline void throw_cannot_use_with(const exception_id id_, const char* operation, const BasicJsonType& j)
|
||||
{
|
||||
static_cast<void>(id_); // unused when JSON_NOEXCEPTION is defined
|
||||
static_cast<void>(operation);
|
||||
@@ -451,18 +451,6 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_use_with(const
|
||||
JSON_THROW(type_error::create(id_, concat("cannot use ", operation, " with ", j.type_name()), &j));
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief throws ExceptionType with a fixed message; kept out of line so that callers do not carry the exception construction
|
||||
*/
|
||||
template<typename ExceptionType, typename BasicJsonContext>
|
||||
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_literal(const exception_id id_, const char* what_arg, BasicJsonContext context)
|
||||
{
|
||||
static_cast<void>(id_); // unused when JSON_NOEXCEPTION is defined
|
||||
static_cast<void>(what_arg);
|
||||
static_cast<void>(context);
|
||||
JSON_THROW(ExceptionType::create(id_, what_arg, context));
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
#include <cstdint> // uint32_t
|
||||
#include <cstring> // strlen
|
||||
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
|
||||
#include <limits> // numeric_limits
|
||||
#include <streambuf> // streambuf
|
||||
#include <string> // string, char_traits
|
||||
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
|
||||
#include <utility> // pair, declval
|
||||
#include <vector> // vector
|
||||
|
||||
#ifndef JSON_NO_IO
|
||||
#include <cstdio> // FILE *
|
||||
@@ -30,6 +32,24 @@
|
||||
#include <nlohmann/detail/meta/type_traits.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
|
||||
namespace detail
|
||||
{
|
||||
@@ -56,18 +76,38 @@ class file_input_adapter
|
||||
: m_file(f)
|
||||
{
|
||||
JSON_ASSERT(m_file != nullptr);
|
||||
lock();
|
||||
}
|
||||
|
||||
// make class move-only
|
||||
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=(file_input_adapter&&) = delete;
|
||||
~file_input_adapter() = default;
|
||||
~file_input_adapter()
|
||||
{
|
||||
if (m_file != nullptr)
|
||||
{
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
#endif
|
||||
}
|
||||
|
||||
// returns the number of characters successfully read
|
||||
@@ -78,6 +118,27 @@ class file_input_adapter
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
#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
|
||||
std::FILE* m_file;
|
||||
};
|
||||
@@ -127,15 +188,26 @@ class input_stream_adapter
|
||||
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
|
||||
: 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.sb = nullptr;
|
||||
#if JSON_PRECISE_STREAM_POSITION
|
||||
rhs.lookahead = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if JSON_PRECISE_STREAM_POSITION
|
||||
// Whether the character last returned by get_character() can be given back
|
||||
// to the input with release_lookahead().
|
||||
static constexpr bool supports_lookahead = true;
|
||||
@@ -155,17 +227,41 @@ class input_stream_adapter
|
||||
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();
|
||||
// set eof manually, as we don't use the istream interface.
|
||||
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
||||
{
|
||||
// there is nothing to step over next time
|
||||
lookahead = false;
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
current_in_get_area = false;
|
||||
#endif
|
||||
is->clear(is->rdstate() | std::ios::eofbit);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -179,13 +275,6 @@ class input_stream_adapter
|
||||
lookahead = false;
|
||||
}
|
||||
#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
|
||||
// ensure that std::char_traits<char>::eof() and the character 0xFF do not
|
||||
// end up as the same value, e.g., 0xFFFFFFFF.
|
||||
@@ -194,12 +283,37 @@ class input_stream_adapter
|
||||
// stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION.
|
||||
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();
|
||||
// set eof manually, as we don't use the istream interface.
|
||||
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);
|
||||
}
|
||||
#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;
|
||||
}
|
||||
#endif
|
||||
@@ -218,7 +332,165 @@ class input_stream_adapter
|
||||
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:
|
||||
#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
|
||||
// Step over the character last returned by get_character(). The character
|
||||
// has already been peeked successfully, so for every streambuf with a get
|
||||
@@ -240,6 +512,16 @@ class input_stream_adapter
|
||||
/// whether get_character() peeked a character that is not consumed yet
|
||||
bool lookahead = false;
|
||||
#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
|
||||
|
||||
|
||||
@@ -674,19 +674,16 @@ class json_sax_dom_callback_parser
|
||||
if (keep && ref_stack.back())
|
||||
{
|
||||
auto& obj = *ref_stack.back()->m_data.m_value.object;
|
||||
// a single lookup: insert the placeholder, or find the existing member
|
||||
const auto inserted = obj.emplace(val, discarded);
|
||||
const auto it = inserted.first;
|
||||
if (!inserted.second)
|
||||
const auto it = obj.find(val);
|
||||
if (it != obj.end())
|
||||
{
|
||||
// this is a duplicate key (legal in JSON); remember its
|
||||
// current value so it can be restored later if the new
|
||||
// value is rejected by the callback, instead of being
|
||||
// erased together with the discarded placeholder
|
||||
duplicate_key_stash.emplace_back(&(it->second), it->second);
|
||||
it->second = discarded;
|
||||
}
|
||||
object_element = &(it->second);
|
||||
object_element = &(obj[val] = discarded);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -166,6 +166,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/)
|
||||
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
|
||||
|
||||
@@ -184,9 +221,20 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
/// whether the last read token can be reconstructed from the input adapter
|
||||
/// on demand (in error paths) instead of being copied on every scanned
|
||||
/// 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> {});
|
||||
|
||||
/// 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
|
||||
/// instead of being counted on every scanned character. Seekable adapters
|
||||
/// 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
|
||||
/// get_position() is called (errors). Counting per character kept two
|
||||
/// 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
|
||||
/// 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> {});
|
||||
|
||||
/// whether string scanning may bulk-consume runs of ordinary characters
|
||||
/// directly from a contiguous input buffer (SWAR fast path). This requires
|
||||
/// the token to be reconstructible lazily (lazy_token_string), so bypassing
|
||||
/// the per-character capture in get() cannot lose error diagnostics. It
|
||||
/// also implies lazy_position, so the bulk paths need not advance the
|
||||
/// position counters.
|
||||
/// directly from a contiguous input buffer (SWAR fast path). Without
|
||||
/// lazy_token_string / lazy_position, the bulk paths copy the consumed
|
||||
/// characters into token_string and advance the position counters
|
||||
/// themselves (see bulk_consume).
|
||||
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:
|
||||
using token_type = typename lexer_base<BasicJsonType>::token_type;
|
||||
@@ -219,7 +270,11 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
: ia(std::move(adapter))
|
||||
, ignore_comments(ignore_comments_)
|
||||
, 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
|
||||
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`
|
||||
/// directly from the input buffer via hex_codepoint(), instead of 4 calls
|
||||
/// to get(). On success, advances the adapter exactly as those 4 get()
|
||||
/// calls would (bulk_scan implies lazy_position, so there are no position
|
||||
/// counters to advance) and leaves @a current holding the last of
|
||||
/// calls would (including the position and token bookkeeping of a
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -256,7 +311,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ia.bulk_skip(4);
|
||||
bulk_consume(reinterpret_cast<const char*>(raw), 4);
|
||||
current = char_traits<char_type>::to_int_type(raw[3]);
|
||||
out = codepoint;
|
||||
return true;
|
||||
@@ -385,7 +440,36 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
return;
|
||||
}
|
||||
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
|
||||
@@ -1603,6 +1687,10 @@ scan_number_done:
|
||||
}
|
||||
}
|
||||
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
|
||||
// to run before the input position advances below
|
||||
@@ -1620,7 +1708,8 @@ scan_number_done:
|
||||
const token_type integer_result = convert_integer(number_type, data, data + len);
|
||||
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;
|
||||
}
|
||||
// The value does not fit an integer, so this token converts as a
|
||||
@@ -1636,11 +1725,37 @@ scan_number_done:
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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
|
||||
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
||||
{
|
||||
@@ -1696,11 +1811,30 @@ scan_number_done:
|
||||
/// seekable adapter: remember where the current token starts so it can be
|
||||
/// reconstructed from the input on error; current has already been
|
||||
/// 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-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
|
||||
/// already-read first character
|
||||
void note_token_start(std::false_type /*lazy*/) noexcept
|
||||
@@ -2021,6 +2155,20 @@ scan_number_done:
|
||||
public:
|
||||
/// 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
|
||||
{
|
||||
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
|
||||
// was consumed from the input but is not part of the token; EOF is
|
||||
@@ -2142,9 +2290,8 @@ scan_number_done:
|
||||
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
|
||||
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
|
||||
fine because bulk_scan implies lazy_position: the position is derived from
|
||||
the adapter, not counted per character.
|
||||
other whitespace are left to that loop. Skipping bypasses get();
|
||||
bulk_consume() does its bookkeeping.
|
||||
*/
|
||||
void skip_indentation(std::true_type /*bulk*/)
|
||||
{
|
||||
@@ -2167,7 +2314,7 @@ scan_number_done:
|
||||
break;
|
||||
}
|
||||
}
|
||||
ia.bulk_skip(pos);
|
||||
bulk_consume(data, pos);
|
||||
}
|
||||
|
||||
/// streaming input: no bulk fast path
|
||||
|
||||
@@ -280,7 +280,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
}
|
||||
|
||||
/// @throw invalid_iterator.214 always
|
||||
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_get_value() const
|
||||
JSON_HEDLEY_NO_RETURN void throw_cannot_get_value() const
|
||||
{
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterator_value_unavailable, "cannot get value", m_object));
|
||||
}
|
||||
@@ -294,25 +294,40 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
{
|
||||
JSON_ASSERT(m_object != nullptr);
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
|
||||
return *m_it.array_iterator;
|
||||
}
|
||||
case value_t::object:
|
||||
{
|
||||
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
|
||||
return m_it.object_iterator->second;
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
|
||||
{
|
||||
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
|
||||
return m_it.object_iterator->second;
|
||||
}
|
||||
case value_t::array:
|
||||
{
|
||||
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
|
||||
return *m_it.array_iterator;
|
||||
}
|
||||
|
||||
// neither array nor object: a primitive value (null has no value to point to)
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type != value_t::null && m_it.primitive_iterator.is_begin()))
|
||||
{
|
||||
return *m_object;
|
||||
}
|
||||
case value_t::null:
|
||||
throw_cannot_get_value();
|
||||
|
||||
throw_cannot_get_value();
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
{
|
||||
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
|
||||
{
|
||||
return *m_object;
|
||||
}
|
||||
|
||||
throw_cannot_get_value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -323,25 +338,38 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
{
|
||||
JSON_ASSERT(m_object != nullptr);
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
|
||||
return &*m_it.array_iterator;
|
||||
}
|
||||
case value_t::object:
|
||||
{
|
||||
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
|
||||
return &(m_it.object_iterator->second);
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
|
||||
{
|
||||
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
|
||||
return &(m_it.object_iterator->second);
|
||||
}
|
||||
case value_t::array:
|
||||
{
|
||||
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
|
||||
return &*m_it.array_iterator;
|
||||
}
|
||||
|
||||
// neither array nor object: a primitive value
|
||||
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
|
||||
{
|
||||
return m_object;
|
||||
}
|
||||
case value_t::null:
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
{
|
||||
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
|
||||
{
|
||||
return m_object;
|
||||
}
|
||||
|
||||
throw_cannot_get_value();
|
||||
throw_cannot_get_value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -363,20 +391,35 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
{
|
||||
JSON_ASSERT(m_object != nullptr);
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
++m_it.array_iterator;
|
||||
return *this;
|
||||
case value_t::object:
|
||||
{
|
||||
std::advance(m_it.object_iterator, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case value_t::array:
|
||||
{
|
||||
std::advance(m_it.array_iterator, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case value_t::null:
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
{
|
||||
++m_it.primitive_iterator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
|
||||
{
|
||||
std::advance(m_it.object_iterator, 1);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// neither array nor object: a primitive value
|
||||
++m_it.primitive_iterator;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -399,20 +442,35 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
{
|
||||
JSON_ASSERT(m_object != nullptr);
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
--m_it.array_iterator;
|
||||
return *this;
|
||||
case value_t::object:
|
||||
{
|
||||
std::advance(m_it.object_iterator, -1);
|
||||
break;
|
||||
}
|
||||
|
||||
case value_t::array:
|
||||
{
|
||||
std::advance(m_it.array_iterator, -1);
|
||||
break;
|
||||
}
|
||||
|
||||
case value_t::null:
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
{
|
||||
--m_it.primitive_iterator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
|
||||
{
|
||||
std::advance(m_it.object_iterator, -1);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// neither array nor object: a primitive value
|
||||
--m_it.primitive_iterator;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -426,7 +484,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
// if objects are not the same, the comparison is undefined
|
||||
if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
|
||||
{
|
||||
throw_literal<invalid_iterator>(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object));
|
||||
}
|
||||
|
||||
// value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493
|
||||
@@ -435,18 +493,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
return true;
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
return (m_it.array_iterator == other.m_it.array_iterator);
|
||||
}
|
||||
case value_t::object:
|
||||
return (m_it.object_iterator == other.m_it.object_iterator);
|
||||
|
||||
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
|
||||
{
|
||||
return (m_it.object_iterator == other.m_it.object_iterator);
|
||||
}
|
||||
case value_t::array:
|
||||
return (m_it.array_iterator == other.m_it.array_iterator);
|
||||
|
||||
// neither array nor object: a primitive value
|
||||
return (m_it.primitive_iterator == other.m_it.primitive_iterator);
|
||||
case value_t::null:
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
return (m_it.primitive_iterator == other.m_it.primitive_iterator);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -468,7 +533,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
// if objects are not the same, the comparison is undefined
|
||||
if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
|
||||
{
|
||||
throw_literal<invalid_iterator>(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object));
|
||||
}
|
||||
|
||||
// value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493
|
||||
@@ -481,7 +546,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
case value_t::object:
|
||||
throw_literal<invalid_iterator>(exception_id::iterator_order_on_object, "cannot compare order of object iterators", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterator_order_on_object, "cannot compare order of object iterators", m_object));
|
||||
|
||||
case value_t::array:
|
||||
return (m_it.array_iterator < other.m_it.array_iterator);
|
||||
@@ -537,7 +602,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
case value_t::object:
|
||||
throw_literal<invalid_iterator>(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object));
|
||||
|
||||
case value_t::array:
|
||||
{
|
||||
@@ -616,7 +681,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
case value_t::object:
|
||||
throw_literal<invalid_iterator>(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object));
|
||||
|
||||
case value_t::array:
|
||||
return m_it.array_iterator - other.m_it.array_iterator;
|
||||
@@ -645,7 +710,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
switch (m_object->m_data.m_type)
|
||||
{
|
||||
case value_t::object:
|
||||
throw_literal<invalid_iterator>(exception_id::iterator_subscript_on_object, "cannot use operator[] for object iterators", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterator_subscript_on_object, "cannot use operator[] for object iterators", m_object));
|
||||
|
||||
case value_t::array:
|
||||
return *std::next(m_it.array_iterator, n);
|
||||
@@ -685,7 +750,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
|
||||
return m_it.object_iterator->first;
|
||||
}
|
||||
|
||||
throw_literal<invalid_iterator>(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators", m_object);
|
||||
JSON_THROW(invalid_iterator::create(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators", m_object));
|
||||
}
|
||||
|
||||
/*!
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <iosfwd> // ostream
|
||||
#endif // JSON_NO_IO
|
||||
#include <limits> // max
|
||||
#include <map> // map
|
||||
#include <numeric> // accumulate
|
||||
#include <string> // string
|
||||
#include <utility> // move
|
||||
#include <vector> // vector
|
||||
@@ -67,21 +69,12 @@ class json_pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/to_string/
|
||||
string_t to_string() const
|
||||
{
|
||||
// one buffer: every token adds its escaped form (at least its length) and a slash
|
||||
std::size_t length = 0;
|
||||
for (const auto& reference_token : reference_tokens)
|
||||
return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
|
||||
string_t{},
|
||||
[](const string_t& a, const string_t& b)
|
||||
{
|
||||
length += reference_token.size() + 1;
|
||||
}
|
||||
|
||||
string_t result;
|
||||
result.reserve(length);
|
||||
for (const auto& reference_token : reference_tokens)
|
||||
{
|
||||
result.append("/", 1);
|
||||
detail::escape_into(result, reference_token);
|
||||
}
|
||||
return result;
|
||||
return detail::concat<string_t>(a, '/', detail::escape(b));
|
||||
});
|
||||
}
|
||||
|
||||
/// @brief return a string representation of the JSON pointer
|
||||
@@ -136,29 +129,21 @@ class json_pointer
|
||||
friend json_pointer operator/(const json_pointer& lhs,
|
||||
const json_pointer& rhs)
|
||||
{
|
||||
json_pointer result;
|
||||
result.reference_tokens.reserve(lhs.reference_tokens.size() + rhs.reference_tokens.size());
|
||||
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
|
||||
result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end());
|
||||
return result;
|
||||
return json_pointer(lhs) /= rhs;
|
||||
}
|
||||
|
||||
/// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
|
||||
friend json_pointer operator/(const json_pointer& lhs, string_t token) // NOLINT(performance-unnecessary-value-param)
|
||||
{
|
||||
json_pointer result;
|
||||
result.reference_tokens.reserve(lhs.reference_tokens.size() + 1);
|
||||
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
|
||||
result.reference_tokens.push_back(std::move(token));
|
||||
return result;
|
||||
return json_pointer(lhs) /= std::move(token);
|
||||
}
|
||||
|
||||
/// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
|
||||
friend json_pointer operator/(const json_pointer& lhs, std::size_t array_idx)
|
||||
{
|
||||
return lhs / detail::to_string<string_t>(array_idx);
|
||||
return json_pointer(lhs) /= array_idx;
|
||||
}
|
||||
|
||||
/// @brief returns the parent of this JSON pointer
|
||||
@@ -170,8 +155,8 @@ class json_pointer
|
||||
return *this;
|
||||
}
|
||||
|
||||
json_pointer res;
|
||||
res.reference_tokens = std::vector<string_t>(reference_tokens.begin(), reference_tokens.end() - 1);
|
||||
json_pointer res = *this;
|
||||
res.pop_back();
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -260,14 +245,14 @@ class json_pointer
|
||||
|
||||
private:
|
||||
/// @throw out_of_range.405 always
|
||||
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NO_RETURN static void throw_no_parent()
|
||||
JSON_HEDLEY_NO_RETURN static void throw_no_parent()
|
||||
{
|
||||
JSON_THROW(detail::out_of_range::create(detail::exception_id::patch_on_root, "JSON pointer has no parent", nullptr));
|
||||
}
|
||||
|
||||
/// @throw out_of_range.404 always
|
||||
template<typename BasicJsonContext>
|
||||
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NO_RETURN static void throw_unresolved(const string_t& reference_token, BasicJsonContext context)
|
||||
JSON_HEDLEY_NO_RETURN static void throw_unresolved(const string_t& reference_token, BasicJsonContext context)
|
||||
{
|
||||
static_cast<void>(reference_token); // unused when JSON_NOEXCEPTION is defined
|
||||
static_cast<void>(context);
|
||||
@@ -320,28 +305,6 @@ class json_pointer
|
||||
return array_index_status::not_a_number;
|
||||
}
|
||||
|
||||
// fast path: up to 18 decimal digits cannot overflow unsigned long long or
|
||||
// size_type (9 digits for a 32-bit size_type), so strtoull and errno are not needed
|
||||
constexpr std::size_t max_fast_digits = sizeof(size_type) >= 8 ? 18 : 9;
|
||||
if (!s.empty() && s.size() <= max_fast_digits)
|
||||
{
|
||||
size_type acc = 0;
|
||||
for (std::size_t i = 0; i < s.size(); ++i)
|
||||
{
|
||||
const char c = s[i];
|
||||
if (JSON_HEDLEY_UNLIKELY(c < '0' || c > '9'))
|
||||
{
|
||||
break; // not a plain number: let strtoull decide
|
||||
}
|
||||
acc = static_cast<size_type>(acc * 10 + static_cast<size_type>(c - '0'));
|
||||
if (JSON_HEDLEY_UNLIKELY(i == s.size() - 1))
|
||||
{
|
||||
idx = acc;
|
||||
return array_index_status::ok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* p = s.data();
|
||||
char* p_end = nullptr; // NOLINT(misc-const-correctness)
|
||||
errno = 0; // strtoull doesn't reset errno
|
||||
@@ -398,83 +361,6 @@ class json_pointer
|
||||
return idx;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare with a JSON pointer string without splitting it into tokens
|
||||
|
||||
@throw parse_error.107 if the string is not empty or begins with '/'
|
||||
@throw parse_error.108 if character '~' is not followed by '0' or '1'
|
||||
*/
|
||||
bool equals_string(const string_t& reference_string) const
|
||||
{
|
||||
if (reference_string.empty())
|
||||
{
|
||||
return reference_tokens.empty();
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/'))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr));
|
||||
}
|
||||
|
||||
// the whole string is validated, whether or not it matches
|
||||
const std::size_t size = reference_string.size();
|
||||
for (std::size_t pos = reference_string.find_first_of('~');
|
||||
pos != string_t::npos;
|
||||
pos = reference_string.find_first_of('~', pos + 1))
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(pos == size - 1 || (reference_string[pos + 1] != '0' && reference_string[pos + 1] != '1')))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
// compare token by token, unescaping the string on the fly
|
||||
std::size_t pos = 0; // the slash in front of the next token
|
||||
for (const auto& reference_token : reference_tokens)
|
||||
{
|
||||
if (pos >= size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t i = pos + 1;
|
||||
std::size_t j = 0;
|
||||
const std::size_t token_size = reference_token.size();
|
||||
while (i < size && reference_string[i] != '/')
|
||||
{
|
||||
char c = reference_string[i++];
|
||||
if (c == '~')
|
||||
{
|
||||
c = (reference_string[i++] == '0') ? '~' : '/';
|
||||
}
|
||||
if (j == token_size || reference_token[j] != c)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (j != token_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pos = i;
|
||||
}
|
||||
return pos == size;
|
||||
}
|
||||
|
||||
/// @brief object member access with a key that is only copied if it is inserted
|
||||
template<typename BasicJsonType>
|
||||
static BasicJsonType& object_member(BasicJsonType& object, const string_t& key)
|
||||
{
|
||||
auto& members = *object.m_data.m_value.object;
|
||||
const auto it = members.find(key);
|
||||
if (it != members.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
return object.operator[](key);
|
||||
}
|
||||
|
||||
JSON_PRIVATE_UNLESS_TESTED:
|
||||
json_pointer top() const
|
||||
{
|
||||
@@ -498,141 +384,84 @@ class json_pointer
|
||||
the result independent of the order in which the flattened object is
|
||||
iterated, which is unspecified for some object types.
|
||||
|
||||
The prefixes form a tree and are numbered (number 0 is the empty prefix),
|
||||
so each of them is stored only once (as a node) rather than as a copy of
|
||||
all of its reference tokens. The tree does not own the reference tokens:
|
||||
it refers to the tokens of the parsed keys, which must outlive it.
|
||||
The prefixes form a tree and are numbered, so each of them is stored only
|
||||
once (as a node) rather than as a copy of all of its reference tokens.
|
||||
*/
|
||||
struct prefix_tree
|
||||
{
|
||||
// open-addressing hash table from (parent, last reference token) to
|
||||
// the number of the node; empty slots have token == nullptr
|
||||
struct slot
|
||||
{
|
||||
std::size_t hash;
|
||||
std::size_t parent;
|
||||
const string_t* token;
|
||||
std::size_t id;
|
||||
|
||||
slot() noexcept
|
||||
: hash(0), parent(0), token(nullptr), id(0)
|
||||
{}
|
||||
|
||||
slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept
|
||||
: hash(h), parent(p), token(t), id(i)
|
||||
{}
|
||||
};
|
||||
|
||||
std::vector<slot> slots;
|
||||
std::size_t used = 0;
|
||||
|
||||
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
|
||||
{
|
||||
std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL);
|
||||
for (std::size_t i = 0; i < token.size(); ++i)
|
||||
{
|
||||
h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL);
|
||||
}
|
||||
return h ^ (h >> 29u);
|
||||
}
|
||||
|
||||
void grow()
|
||||
{
|
||||
std::vector<slot> old(slots.size() * 2);
|
||||
old.swap(slots);
|
||||
const std::size_t mask = slots.size() - 1;
|
||||
for (const auto& s : old)
|
||||
{
|
||||
if (s.token != nullptr)
|
||||
{
|
||||
std::size_t i = s.hash & mask;
|
||||
while (slots[i].token != nullptr)
|
||||
{
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
slots[i] = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parent[id] is the number of the prefix without its last token
|
||||
std::vector<std::size_t> parent;
|
||||
// children[id] maps a reference token to the number of the prefix
|
||||
// extended by that token; number 0 is the empty prefix
|
||||
std::vector<std::map<string_t, std::size_t>> children;
|
||||
// is_array[id] is true iff some flattened key has the reference token
|
||||
// 0 directly below the prefix with number id
|
||||
std::vector<bool> is_array;
|
||||
|
||||
// start with the empty prefix only
|
||||
prefix_tree()
|
||||
: slots(16)
|
||||
, parent(1, 0)
|
||||
: children(1)
|
||||
, is_array(1, false)
|
||||
{}
|
||||
|
||||
// return the number of the prefix with number id extended by
|
||||
// reference_token, adding it if it is new
|
||||
std::size_t add_child(std::size_t id, const string_t& reference_token)
|
||||
std::size_t add_child(std::size_t id, string_t&& reference_token)
|
||||
{
|
||||
const std::size_t hash = hash_of(id, reference_token);
|
||||
const std::size_t mask = slots.size() - 1;
|
||||
std::size_t i = hash & mask;
|
||||
while (slots[i].token != nullptr)
|
||||
{
|
||||
if (slots[i].hash == hash && slots[i].parent == id && *slots[i].token == reference_token)
|
||||
{
|
||||
return slots[i].id;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
|
||||
// new prefix
|
||||
const std::size_t child = parent.size();
|
||||
slots[i] = slot(hash, id, &reference_token, child);
|
||||
if (reference_token == "0")
|
||||
{
|
||||
is_array[id] = true;
|
||||
}
|
||||
parent.push_back(id);
|
||||
is_array.push_back(false);
|
||||
|
||||
// keep the load factor below 1/2
|
||||
if (++used * 2 > slots.size())
|
||||
// read the number before the emplace_back below, which may
|
||||
// reallocate children and invalidate the iterator
|
||||
const std::size_t next = children.size();
|
||||
const auto inserted = children[id].emplace(std::move(reference_token), next);
|
||||
const std::size_t child = inserted.first->second;
|
||||
if (inserted.second)
|
||||
{
|
||||
grow();
|
||||
children.emplace_back();
|
||||
is_array.push_back(false);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
// return the number of the prefix with number id extended by
|
||||
// reference_token, which must have been added before
|
||||
std::size_t find_child(std::size_t id, const string_t& reference_token) const
|
||||
{
|
||||
const auto it = children[id].find(reference_token);
|
||||
JSON_ASSERT(it != children[id].end());
|
||||
return it->second;
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief create and return a reference to the pointed to value
|
||||
|
||||
@param[in] j the value to create the pointed to value in
|
||||
@param[in] tokens the reference tokens of the pointer
|
||||
@param[in] prefixes prefixes[i] is the number in @a tree of the prefix
|
||||
consisting of the first i reference tokens
|
||||
|
||||
Complexity: Linear in the number of reference tokens.
|
||||
Complexity: Linear in the number of reference tokens (times the logarithm
|
||||
of the number of siblings for the prefix lookup).
|
||||
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if array index is not a number
|
||||
@throw type_error.313 if value cannot be unflattened
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens,
|
||||
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
|
||||
BasicJsonType& get_and_create(BasicJsonType& j, const prefix_tree& tree) const
|
||||
{
|
||||
auto* result = &j;
|
||||
|
||||
// the number of the prefix consumed so far; used to look up whether
|
||||
// the value to be created below is an array or an object
|
||||
std::size_t id = 0;
|
||||
|
||||
// in case no reference tokens exist, return a reference to the JSON value
|
||||
// j which will be overwritten by a primitive value
|
||||
for (std::size_t i = 0; i < tokens.size(); ++i)
|
||||
for (const auto& reference_token : reference_tokens)
|
||||
{
|
||||
const auto& reference_token = tokens[i];
|
||||
switch (result->type())
|
||||
{
|
||||
case detail::value_t::null:
|
||||
{
|
||||
if (tree.is_array[prefixes[i]])
|
||||
if (tree.is_array[id])
|
||||
{
|
||||
// some reference token below this position is 0, so the
|
||||
// value is an array
|
||||
@@ -649,7 +478,7 @@ class json_pointer
|
||||
case detail::value_t::object:
|
||||
{
|
||||
// create an entry in the object
|
||||
result = &object_member(*result, reference_token);
|
||||
result = &result->operator[](reference_token);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -676,6 +505,8 @@ class json_pointer
|
||||
default:
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j));
|
||||
}
|
||||
|
||||
id = tree.find_child(id, reference_token);
|
||||
}
|
||||
|
||||
return *result;
|
||||
@@ -727,7 +558,7 @@ class json_pointer
|
||||
case detail::value_t::object:
|
||||
{
|
||||
// use unchecked object access
|
||||
ptr = &object_member(*ptr, reference_token);
|
||||
ptr = &ptr->operator[](reference_token);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -777,13 +608,8 @@ class json_pointer
|
||||
{
|
||||
case detail::value_t::object:
|
||||
{
|
||||
const auto it = ptr->m_data.m_value.object->find(reference_token);
|
||||
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
|
||||
{
|
||||
// at() throws out_of_range.403 with the usual message
|
||||
ptr->at(reference_token);
|
||||
}
|
||||
ptr = &it->second;
|
||||
// note: at performs range check
|
||||
ptr = &ptr->at(reference_token);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -804,7 +630,7 @@ class json_pointer
|
||||
JSON_THROW(detail::out_of_range::create(detail::exception_id::array_index_out_of_range, detail::concat(
|
||||
"array index ", std::to_string(idx), " is out of range"), ptr));
|
||||
}
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
ptr = &ptr->operator[](idx);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -852,9 +678,7 @@ class json_pointer
|
||||
{
|
||||
// use unchecked object access; the const operator[]
|
||||
// asserts that the key exists
|
||||
const auto it = ptr->m_data.m_value.object->find(reference_token);
|
||||
JSON_ASSERT(it != ptr->m_data.m_value.object->end());
|
||||
ptr = &it->second;
|
||||
ptr = &ptr->operator[](reference_token);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -868,9 +692,7 @@ class json_pointer
|
||||
|
||||
// use unchecked array access; the const operator[]
|
||||
// asserts that the index exists
|
||||
const auto idx = array_index<BasicJsonType>(reference_token);
|
||||
JSON_ASSERT(idx < ptr->m_data.m_value.array->size());
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -911,12 +733,12 @@ class json_pointer
|
||||
{
|
||||
case detail::value_t::object:
|
||||
{
|
||||
const auto it = ptr->m_data.m_value.object->find(reference_token);
|
||||
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
|
||||
const auto it = ptr->find(reference_token);
|
||||
if (JSON_HEDLEY_UNLIKELY(it == ptr->end()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr = &it->second;
|
||||
ptr = &*it;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -953,7 +775,7 @@ class json_pointer
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
ptr = &ptr->operator[](idx);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -986,14 +808,13 @@ class json_pointer
|
||||
{
|
||||
case detail::value_t::object:
|
||||
{
|
||||
const auto it = ptr->m_data.m_value.object->find(reference_token);
|
||||
if (it == ptr->m_data.m_value.object->end())
|
||||
if (!ptr->contains(reference_token))
|
||||
{
|
||||
// we did not find the key in the object
|
||||
return false;
|
||||
}
|
||||
|
||||
ptr = &it->second;
|
||||
ptr = &ptr->operator[](reference_token);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1015,13 +836,13 @@ class json_pointer
|
||||
return false;
|
||||
}
|
||||
|
||||
if (idx >= ptr->m_data.m_value.array->size())
|
||||
if (idx >= ptr->size())
|
||||
{
|
||||
// index out of range
|
||||
return false;
|
||||
}
|
||||
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
ptr = &ptr->operator[](idx);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1071,15 +892,6 @@ class json_pointer
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr));
|
||||
}
|
||||
|
||||
// every slash starts a reference token
|
||||
const std::size_t size = reference_string.size();
|
||||
std::size_t token_count = 0;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
{
|
||||
token_count += (reference_string[i] == '/') ? 1 : 0;
|
||||
}
|
||||
result.reserve(token_count);
|
||||
|
||||
// extract the reference tokens:
|
||||
// - slash: position of the last read slash (or end of string)
|
||||
// - start: position after the previous slash
|
||||
@@ -1101,52 +913,31 @@ class json_pointer
|
||||
const auto count = (slash == string_t::npos ? reference_string.size() : slash) - start;
|
||||
auto reference_token = string_t(reference_string.data() + start, count);
|
||||
|
||||
// check reference tokens are properly escaped; a token without
|
||||
// a '~' (the common case) needs neither check nor unescaping
|
||||
std::size_t pos = reference_token.find_first_of('~');
|
||||
if (pos != string_t::npos)
|
||||
// check reference tokens are properly escaped
|
||||
for (std::size_t pos = reference_token.find_first_of('~');
|
||||
pos != string_t::npos;
|
||||
pos = reference_token.find_first_of('~', pos + 1))
|
||||
{
|
||||
for (; pos != string_t::npos; pos = reference_token.find_first_of('~', pos + 1))
|
||||
JSON_ASSERT(reference_token[pos] == '~');
|
||||
|
||||
// ~ must be followed by 0 or 1
|
||||
if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 ||
|
||||
(reference_token[pos + 1] != '0' &&
|
||||
reference_token[pos + 1] != '1')))
|
||||
{
|
||||
JSON_ASSERT(reference_token[pos] == '~');
|
||||
|
||||
// ~ must be followed by 0 or 1
|
||||
if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 ||
|
||||
(reference_token[pos + 1] != '0' &&
|
||||
reference_token[pos + 1] != '1')))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
|
||||
}
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
|
||||
}
|
||||
|
||||
detail::unescape(reference_token);
|
||||
}
|
||||
|
||||
// finally, store the reference token
|
||||
result.push_back(std::move(reference_token));
|
||||
detail::unescape(reference_token);
|
||||
result.push_back(reference_token);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
// Add the entry (key, v) to the object `obj`. Flattened paths are pairwise
|
||||
// distinct, so objects that can append a key known to be absent
|
||||
// (ordered_map) skip the lookup, which is linear and would make flatten
|
||||
// quadratic; all other object types use operator[].
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, int /*unused*/)
|
||||
-> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void())
|
||||
{
|
||||
obj.emplace_new_key(key, std::forward<ValueType>(v));
|
||||
}
|
||||
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, ...)
|
||||
{
|
||||
obj[key] = std::forward<ValueType>(v);
|
||||
}
|
||||
|
||||
/*!
|
||||
@param[in] reference_string the reference string to the current value
|
||||
@param[in] value the value to consider
|
||||
@@ -1191,8 +982,7 @@ class json_pointer
|
||||
|
||||
// flatten `v`, whose path is `path`: primitives and empty containers
|
||||
// are added to the result right away; other containers get a frame
|
||||
auto& object = *result.m_data.m_value.object;
|
||||
const auto enter = [&stack, &path, &object](const BasicJsonType & v)
|
||||
const auto enter = [&stack, &path, &result](const BasicJsonType & v)
|
||||
{
|
||||
switch (v.type())
|
||||
{
|
||||
@@ -1201,7 +991,7 @@ class json_pointer
|
||||
if (v.m_data.m_value.array->empty())
|
||||
{
|
||||
// flatten empty array as null
|
||||
add_flattened(object, path, nullptr, 0);
|
||||
result[path] = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1215,7 +1005,7 @@ class json_pointer
|
||||
if (v.m_data.m_value.object->empty())
|
||||
{
|
||||
// flatten empty object as null
|
||||
add_flattened(object, path, nullptr, 0);
|
||||
result[path] = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1235,7 +1025,7 @@ class json_pointer
|
||||
default:
|
||||
{
|
||||
// add a primitive value with its reference string
|
||||
add_flattened(object, path, v, 0);
|
||||
result[path] = v;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1281,10 +1071,6 @@ class json_pointer
|
||||
enter(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
// the entries were added without going through basic_json, so the
|
||||
// parent pointers (JSON_DIAGNOSTICS) have to be set now
|
||||
result.set_parents();
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -1308,53 +1094,21 @@ class json_pointer
|
||||
|
||||
BasicJsonType result;
|
||||
|
||||
// parse every key once; the reference tokens are kept for the second
|
||||
// pass (the vectors are never resized, so the tree may refer to them)
|
||||
std::vector<std::vector<string_t>> keys;
|
||||
keys.reserve(value.m_data.m_value.object->size());
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
json_pointer ptr(element.first);
|
||||
keys.push_back(std::move(ptr.reference_tokens));
|
||||
}
|
||||
|
||||
// collect the pointer prefixes that have a reference token 0 among
|
||||
// their children; the values below them are arrays, all others are
|
||||
// objects (see prefix_tree)
|
||||
prefix_tree tree;
|
||||
// the number of the prefix with i + 1 tokens of the previous key; keys
|
||||
// are often sorted, so most of the prefix is the same as for the
|
||||
// previous key and need not be looked up again
|
||||
std::vector<std::size_t> path;
|
||||
const std::vector<string_t>* previous = nullptr;
|
||||
// the number of the complete prefix (i.e., the key) of each key
|
||||
std::vector<std::size_t> leaves;
|
||||
leaves.reserve(keys.size());
|
||||
for (const auto& tokens : keys)
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
std::size_t common = 0;
|
||||
if (previous != nullptr)
|
||||
json_pointer ptr(element.first);
|
||||
std::size_t id = 0;
|
||||
for (auto& reference_token : ptr.reference_tokens)
|
||||
{
|
||||
const std::size_t limit = (std::min)(tokens.size(), previous->size());
|
||||
while (common < limit && tokens[common] == (*previous)[common])
|
||||
{
|
||||
++common;
|
||||
}
|
||||
id = tree.add_child(id, std::move(reference_token));
|
||||
}
|
||||
path.resize(common);
|
||||
std::size_t id = common == 0 ? 0 : path[common - 1];
|
||||
for (std::size_t i = common; i < tokens.size(); ++i)
|
||||
{
|
||||
id = tree.add_child(id, tokens[i]);
|
||||
path.push_back(id);
|
||||
}
|
||||
leaves.push_back(id);
|
||||
previous = &tokens;
|
||||
}
|
||||
|
||||
// iterate the JSON object values
|
||||
std::vector<std::size_t> prefixes;
|
||||
std::size_t key_index = 0;
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
|
||||
@@ -1362,21 +1116,11 @@ class json_pointer
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second));
|
||||
}
|
||||
|
||||
const auto& tokens = keys[key_index];
|
||||
// number the prefixes of this key from its last one to the first
|
||||
prefixes.resize(tokens.size());
|
||||
for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i)
|
||||
{
|
||||
id = tree.parent[id];
|
||||
prefixes[i - 1] = id;
|
||||
}
|
||||
++key_index;
|
||||
|
||||
// Assign the value to the reference pointed to by JSON pointer. Note
|
||||
// that if the JSON pointer is "" (i.e., points to the whole value),
|
||||
// function get_and_create returns a reference to the result itself.
|
||||
// An assignment will then create a primitive value.
|
||||
get_and_create(result, tokens, prefixes, tree) = element.second;
|
||||
json_pointer(element.first).get_and_create(result, tree) = element.second;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1415,7 +1159,7 @@ class json_pointer
|
||||
= delete;
|
||||
#else
|
||||
{
|
||||
return equals_string(rhs);
|
||||
return *this == json_pointer(rhs);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1499,7 +1243,7 @@ inline bool operator==(const json_pointer<RefStringTypeLhs>& lhs,
|
||||
= delete;
|
||||
#else
|
||||
{
|
||||
return lhs.equals_string(rhs);
|
||||
return lhs == json_pointer<RefStringTypeLhs>(rhs);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1512,7 +1256,7 @@ inline bool operator==(const StringType& lhs,
|
||||
= delete;
|
||||
#else
|
||||
{
|
||||
return rhs.equals_string(lhs);
|
||||
return json_pointer<RefStringTypeRhs>(lhs) == rhs;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -895,3 +895,11 @@
|
||||
#ifndef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
|
||||
#define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 0
|
||||
#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
|
||||
@@ -21,6 +21,9 @@
|
||||
#undef JSON_NO_UNIQUE_ADDRESS
|
||||
#undef JSON_DISABLE_ENUM_SERIALIZATION
|
||||
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
|
||||
#undef JSON_DISABLE_STREAM_BULK_SCAN
|
||||
#undef JSON_DISABLE_UNLOCKED_FILE_IO
|
||||
#undef JSON_UNLOCKED_FILE_IO
|
||||
#undef JSON_DTOA_SSE2
|
||||
#undef JSON_DTOA_NEON
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef> // size_t
|
||||
#include <utility> // move
|
||||
|
||||
#include <nlohmann/detail/abi_macros.hpp>
|
||||
|
||||
@@ -25,46 +24,39 @@ namespace detail
|
||||
* Note the order of escaping "~" to "~0" and "/" to "~1" is important.
|
||||
*
|
||||
* The string is rebuilt in a single pass, appending whole runs between the
|
||||
* characters that need escaping, while repeated replace() calls would move
|
||||
* the tail of the string once per escaped character. @ref escape_into appends
|
||||
* the escaped string to an existing one.
|
||||
* characters that need escaping. Scanning with find_first_of() keeps the
|
||||
* common case -- nothing to escape -- as fast as a single search, while
|
||||
* repeated replace() calls would move the tail of the string once per
|
||||
* escaped character.
|
||||
*/
|
||||
template<typename StringType>
|
||||
inline void escape_into(StringType& out, const StringType& s)
|
||||
{
|
||||
const std::size_t size = s.size();
|
||||
std::size_t run = 0;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
{
|
||||
const char c = s[i];
|
||||
if (c == '~' || c == '/')
|
||||
{
|
||||
out.append(s.data() + run, i - run);
|
||||
out.append(c == '~' ? "~0" : "~1", 2);
|
||||
run = i + 1;
|
||||
}
|
||||
}
|
||||
out.append(s.data() + run, size - run);
|
||||
}
|
||||
|
||||
template<typename StringType>
|
||||
inline StringType escape(const StringType& s)
|
||||
{
|
||||
// a single scan for both special characters; nothing to escape is the common case
|
||||
const std::size_t size = s.size();
|
||||
std::size_t i = 0;
|
||||
while (i < size && s[i] != '~' && s[i] != '/')
|
||||
auto next_special = [&s](std::size_t from)
|
||||
{
|
||||
++i;
|
||||
}
|
||||
if (i == size)
|
||||
const auto tilde = s.find_first_of('~', from);
|
||||
const auto slash = s.find_first_of('/', from);
|
||||
return tilde < slash ? tilde : slash; // npos is the largest value
|
||||
};
|
||||
|
||||
auto pos = next_special(0);
|
||||
if (pos == StringType::npos)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
StringType result;
|
||||
result.reserve(size + 2);
|
||||
escape_into(result, s);
|
||||
result.reserve(s.size() + 2);
|
||||
|
||||
std::size_t run = 0;
|
||||
while (pos != StringType::npos)
|
||||
{
|
||||
result.append(s.data() + run, pos - run);
|
||||
result.append(s[pos] == '~' ? "~0" : "~1", 2);
|
||||
run = pos + 1;
|
||||
pos = next_special(run);
|
||||
}
|
||||
result.append(s.data() + run, s.size() - run);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -109,7 +101,7 @@ inline void unescape(StringType& s)
|
||||
pos = s.find_first_of('~', run);
|
||||
}
|
||||
result.append(s.data() + run, s.size() - run);
|
||||
s = std::move(result);
|
||||
s = result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
+269
-624
File diff suppressed because it is too large.
Load diff
@@ -92,19 +92,13 @@ private:
|
||||
|
||||
/// @brief shared implementation of the const and non-const at() overloads
|
||||
/// @throw std::out_of_range if @a key is not found
|
||||
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE
|
||||
static void throw_key_not_found()
|
||||
{
|
||||
JSON_THROW(std::out_of_range("key not found"));
|
||||
}
|
||||
|
||||
template<typename Self, typename KeyType>
|
||||
static auto at_impl(Self& self, const KeyType& key) -> decltype((self.begin()->second))
|
||||
{
|
||||
const auto it = find_impl(self, key);
|
||||
if (it == self.end())
|
||||
{
|
||||
throw_key_not_found();
|
||||
JSON_THROW(std::out_of_range("key not found"));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
@@ -149,17 +143,6 @@ public:
|
||||
return {std::prev(this->end()), true};
|
||||
}
|
||||
|
||||
/// @brief add an element at the end without checking whether the key exists
|
||||
/// @note The caller must guarantee that @a key is not yet contained, otherwise
|
||||
/// the container holds duplicate keys. This avoids the linear search of
|
||||
/// emplace() and operator[] when many keys known to be new are added.
|
||||
template<class V, detail::enable_if_t<
|
||||
detail::is_constructible<T, V>::value, int> = 0>
|
||||
void emplace_new_key(const key_type& key, V && t)
|
||||
{
|
||||
append(key, std::forward<V>(t));
|
||||
}
|
||||
|
||||
T& operator[](const key_type& key)
|
||||
{
|
||||
return emplace(key, T{}).first->second;
|
||||
|
||||
Generated
+1023
-1283
File diff suppressed because it is too large.
Load diff
@@ -285,6 +285,10 @@ if (JSON_TestSimdutf)
|
||||
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
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
# *DO NOT* use json_test_set_test_options() below this line
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <sstream>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <test_data.hpp>
|
||||
|
||||
@@ -51,6 +50,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, 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
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
@@ -624,27 +682,4 @@ static void FromCborChunkedString(benchmark::State& state, const std::size_t chu
|
||||
|
||||
BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000);
|
||||
|
||||
// conversion of a JSON array into standard containers
|
||||
template<typename Container>
|
||||
static void GetArray(benchmark::State& state)
|
||||
{
|
||||
json j = json::array();
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
j.push_back(i);
|
||||
}
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto c = j.get<Container>();
|
||||
benchmark::DoNotOptimize(c);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * 1000);
|
||||
}
|
||||
|
||||
BENCHMARK_TEMPLATE(GetArray, std::vector<int>);
|
||||
BENCHMARK_TEMPLATE(GetArray, std::deque<int>);
|
||||
BENCHMARK_TEMPLATE(GetArray, std::set<int>);
|
||||
BENCHMARK_TEMPLATE(GetArray, std::array<int, 1000>);
|
||||
|
||||
BENCHMARK_MAIN();
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -1150,157 +1149,3 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("comparison of scalars (inline fast path)")
|
||||
{
|
||||
const json::number_integer_t neg = -1;
|
||||
const json::number_integer_t pos = 5;
|
||||
const json::number_unsigned_t big = (std::numeric_limits<json::number_unsigned_t>::max)();
|
||||
|
||||
SECTION("same type")
|
||||
{
|
||||
CHECK(json(pos) == json(pos));
|
||||
CHECK(json(pos) != json(neg));
|
||||
CHECK(json(neg) < json(pos));
|
||||
CHECK_FALSE(json(pos) < json(neg));
|
||||
CHECK_FALSE(json(pos) < json(pos));
|
||||
CHECK(json(pos) <= json(pos));
|
||||
|
||||
CHECK(json(1u) == json(1u));
|
||||
CHECK(json(1u) < json(2u));
|
||||
CHECK_FALSE(json(2u) < json(1u));
|
||||
|
||||
CHECK(json(1.5) == json(1.5));
|
||||
CHECK(json(1.5) < json(2.5));
|
||||
CHECK_FALSE(json(2.5) < json(1.5));
|
||||
CHECK(json(-0.0) == json(0.0));
|
||||
}
|
||||
|
||||
SECTION("mixed number types")
|
||||
{
|
||||
// negative signed vs unsigned must not wrap around
|
||||
CHECK(json(neg) < json(1u));
|
||||
CHECK(json(1u) > json(neg));
|
||||
CHECK(json(neg) != json(big));
|
||||
CHECK(json(neg) < json(big));
|
||||
CHECK_FALSE(json(big) < json(neg));
|
||||
|
||||
CHECK(json(pos) == json(5u));
|
||||
CHECK(json(5u) == json(pos));
|
||||
CHECK(json(pos) < json(6u));
|
||||
CHECK(json(pos) == json(5.0));
|
||||
CHECK(json(5.0) == json(pos));
|
||||
CHECK(json(5u) == json(5.0));
|
||||
CHECK(json(neg) < json(0.5));
|
||||
CHECK(json(0.5) < json(1u));
|
||||
}
|
||||
|
||||
SECTION("boolean and null")
|
||||
{
|
||||
CHECK(json(true) == json(true));
|
||||
CHECK(json(true) != json(false));
|
||||
CHECK(json(false) < json(true));
|
||||
CHECK_FALSE(json(true) < json(false));
|
||||
CHECK_FALSE(json(true) < json(true));
|
||||
|
||||
CHECK(json(nullptr) == json(nullptr));
|
||||
CHECK_FALSE(json(nullptr) < json(nullptr));
|
||||
CHECK(json(nullptr) <= json(nullptr));
|
||||
CHECK(json(nullptr) < json(false));
|
||||
CHECK(json(nullptr) != json(false));
|
||||
CHECK(json(nullptr) != json(0));
|
||||
}
|
||||
|
||||
SECTION("string vs number")
|
||||
{
|
||||
CHECK(json("5") != json(5));
|
||||
CHECK(json(5) != json("5"));
|
||||
CHECK(json(5) < json("5")); // number < string by type order
|
||||
CHECK_FALSE(json("5") < json(5));
|
||||
CHECK(json(1.5) < json(""));
|
||||
CHECK(json("a") == json("a"));
|
||||
CHECK(json("a") < json("b"));
|
||||
}
|
||||
|
||||
SECTION("NaN")
|
||||
{
|
||||
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
|
||||
const json one = 1.0;
|
||||
|
||||
CHECK_FALSE(nan == nan);
|
||||
CHECK(nan != nan);
|
||||
CHECK_FALSE(nan < nan);
|
||||
CHECK_FALSE(nan <= nan);
|
||||
CHECK_FALSE(nan >= nan);
|
||||
CHECK_FALSE(nan > nan);
|
||||
|
||||
CHECK_FALSE(nan == one);
|
||||
CHECK_FALSE(one == nan);
|
||||
CHECK_FALSE(nan < one);
|
||||
CHECK_FALSE(one < nan);
|
||||
CHECK_FALSE(nan <= one);
|
||||
CHECK_FALSE(one <= nan);
|
||||
CHECK_FALSE(nan >= one);
|
||||
CHECK_FALSE(one >= nan);
|
||||
|
||||
// NaN against other number types
|
||||
CHECK_FALSE(nan == json(1));
|
||||
CHECK_FALSE(json(1) == nan);
|
||||
CHECK_FALSE(nan < json(1u));
|
||||
CHECK_FALSE(json(1u) < nan);
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("discarded")
|
||||
{
|
||||
const json d(json::value_t::discarded);
|
||||
|
||||
CHECK_FALSE(d == d);
|
||||
CHECK_FALSE(d < d);
|
||||
CHECK_FALSE(d == json(1));
|
||||
CHECK_FALSE(json(1) == d);
|
||||
CHECK_FALSE(d < json(1));
|
||||
CHECK_FALSE(json(1) < d);
|
||||
CHECK_FALSE(d == json(nullptr));
|
||||
CHECK_FALSE(json(nullptr) == d);
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("scalar overloads")
|
||||
{
|
||||
CHECK(json(5) == 5);
|
||||
CHECK(5 == json(5));
|
||||
CHECK(json(5) < 6);
|
||||
CHECK(4 < json(5));
|
||||
CHECK(json(1.5) == 1.5);
|
||||
CHECK(json(true) == true);
|
||||
CHECK(json(5) != 6);
|
||||
CHECK(json(1u) == 1.0);
|
||||
}
|
||||
|
||||
SECTION("structured values still compare")
|
||||
{
|
||||
CHECK(json({1, 2}) == json({1, 2}));
|
||||
CHECK(json({1, 2}) < json({1, 3}));
|
||||
CHECK(json({{"a", 1}}) == json({{"a", 1}}));
|
||||
CHECK(json::binary({1, 2}) == json::binary({1, 2}));
|
||||
CHECK(json(1) < json::array());
|
||||
CHECK(json::object() < json::array());
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
@@ -187,193 +184,3 @@ TEST_CASE("other constructors and destructor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("copy constructor and destructor across value types")
|
||||
{
|
||||
SECTION("copy of a scalar is independent of the source")
|
||||
{
|
||||
const json src_null = nullptr;
|
||||
const json src_bool = true;
|
||||
const json src_int = -7;
|
||||
const json src_uint = 7u;
|
||||
const json src_float = 2.5;
|
||||
|
||||
json c_null(src_null);
|
||||
json c_bool(src_bool);
|
||||
json c_int(src_int);
|
||||
json c_uint(src_uint);
|
||||
json c_float(src_float);
|
||||
|
||||
CHECK(c_null.is_null());
|
||||
CHECK(c_bool == true);
|
||||
CHECK(c_int == -7);
|
||||
CHECK(c_uint == 7u);
|
||||
CHECK(c_float == 2.5);
|
||||
|
||||
c_int = 8;
|
||||
c_float = 3.5;
|
||||
CHECK(src_int == -7);
|
||||
CHECK(src_float == 2.5);
|
||||
}
|
||||
|
||||
SECTION("copy of an owning value, then mutate the copy")
|
||||
{
|
||||
const json src_string = "hello";
|
||||
const json src_array = {1, 2, 3};
|
||||
const json src_object = {{"a", 1}, {"b", {{"c", 2}}}};
|
||||
const json src_binary = json::binary({1, 2, 3});
|
||||
|
||||
json c_string(src_string);
|
||||
json c_array(src_array);
|
||||
json c_object(src_object);
|
||||
json c_binary(src_binary);
|
||||
|
||||
CHECK(c_string == src_string);
|
||||
CHECK(c_array == src_array);
|
||||
CHECK(c_object == src_object);
|
||||
CHECK(c_binary == src_binary);
|
||||
|
||||
c_string.get_ref<json::string_t&>() += " world";
|
||||
c_array.push_back(4);
|
||||
c_object["b"]["c"] = 3;
|
||||
c_object["d"] = 4;
|
||||
c_binary.get_binary().push_back(4);
|
||||
|
||||
CHECK(src_string == "hello");
|
||||
CHECK(src_array.size() == 3);
|
||||
CHECK(src_object["b"]["c"] == 2);
|
||||
CHECK_FALSE(src_object.contains("d"));
|
||||
CHECK(src_binary.get_binary().size() == 3);
|
||||
|
||||
CHECK(c_string == "hello world");
|
||||
CHECK(c_array.size() == 4);
|
||||
CHECK(c_object["b"]["c"] == 3);
|
||||
CHECK(c_binary.get_binary().size() == 4);
|
||||
}
|
||||
|
||||
SECTION("copy keeps parent pointers valid (JSON_DIAGNOSTICS)")
|
||||
{
|
||||
const json src = {{"a", {1, 2}}, {"b", "x"}};
|
||||
json c(src);
|
||||
c["a"].push_back(3);
|
||||
CHECK(c["a"].size() == 3);
|
||||
CHECK(src["a"].size() == 2);
|
||||
}
|
||||
|
||||
SECTION("vector of mixed types: build, copy, and destroy")
|
||||
{
|
||||
{
|
||||
std::vector<json> v;
|
||||
for (int i = 0; i < 70; ++i)
|
||||
{
|
||||
switch (i % 9)
|
||||
{
|
||||
case 0:
|
||||
v.emplace_back(nullptr);
|
||||
break;
|
||||
case 1:
|
||||
v.emplace_back(i % 2 == 0);
|
||||
break;
|
||||
case 2:
|
||||
v.emplace_back(-i);
|
||||
break;
|
||||
case 3:
|
||||
v.emplace_back(static_cast<json::number_unsigned_t>(i));
|
||||
break;
|
||||
case 4:
|
||||
v.emplace_back(i * 0.5);
|
||||
break;
|
||||
case 5:
|
||||
v.emplace_back(std::string(static_cast<std::size_t>(i), 'x'));
|
||||
break;
|
||||
case 6:
|
||||
v.emplace_back(json::array({i, "s", nullptr}));
|
||||
break;
|
||||
case 7:
|
||||
v.emplace_back(json::object({{"k", i}}));
|
||||
break;
|
||||
default:
|
||||
v.emplace_back(json::binary({1, 2, 3}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(v.size() == 70);
|
||||
|
||||
std::vector<json> copy(v);
|
||||
CHECK(copy == v);
|
||||
|
||||
// mutating the copy leaves the original alone
|
||||
copy[5] = "changed";
|
||||
copy[6].push_back(1);
|
||||
CHECK(copy != v);
|
||||
CHECK(v[5] == std::string(5, 'x'));
|
||||
CHECK(v[6].size() == 3);
|
||||
|
||||
v.erase(v.begin(), v.begin() + 10);
|
||||
CHECK(v.size() == 60);
|
||||
}
|
||||
|
||||
// destroying the discarded and default-constructed values
|
||||
std::vector<json> w(4);
|
||||
w.emplace_back(json::value_t::discarded);
|
||||
w.emplace_back(json::value_t::object);
|
||||
w.emplace_back(json::value_t::array);
|
||||
w.emplace_back(json::value_t::string);
|
||||
w.emplace_back(json::value_t::binary);
|
||||
CHECK(w.size() == 9);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("destructor of nested containers", Json, nlohmann::json, nlohmann::ordered_json)
|
||||
{
|
||||
SECTION("empty containers")
|
||||
{
|
||||
{
|
||||
Json j = Json::array();
|
||||
}
|
||||
{
|
||||
Json j = Json::object();
|
||||
}
|
||||
{
|
||||
Json j = Json::array({Json::array(), Json::object(), Json::array({Json::array()})});
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("leaves mixed with nested containers")
|
||||
{
|
||||
Json j = Json::object();
|
||||
j["leaf"] = 1;
|
||||
j["str"] = "text";
|
||||
j["empty_a"] = Json::array();
|
||||
j["empty_o"] = Json::object();
|
||||
j["arr"] = Json::array({1, "a", Json::array({1, 2}), Json::object({{"k", Json::array({Json::object()})}}), nullptr});
|
||||
j["obj"] = Json::object({{"x", Json::object({{"y", Json::array({1, Json::object({{"z", 1}})})}})}, {"w", 2}});
|
||||
Json copy = j;
|
||||
CHECK(copy == j);
|
||||
j = Json::array({std::move(copy), j, 3, Json::array()});
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
for (int kind = 0; kind < 3; ++kind)
|
||||
{
|
||||
Json j = 1;
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case 0:
|
||||
j = Json::array({std::move(j)});
|
||||
break;
|
||||
case 1:
|
||||
j = Json::object({{"k", std::move(j)}});
|
||||
break;
|
||||
default:
|
||||
// siblings next to the nested child
|
||||
j = (i % 2 == 0) ? Json::array({1, Json::array(), std::move(j)}) : Json::object({{"a", 1}, {"b", Json::object()}, {"c", std::move(j)}});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,9 @@
|
||||
#endif
|
||||
|
||||
#define JSON_DIAGNOSTICS 1
|
||||
#define JSON_TESTS_PRIVATE
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
@@ -633,175 +631,3 @@ TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch()
|
||||
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// check that every child's parent pointer points to the containing value
|
||||
template<typename BasicJsonType>
|
||||
bool parents_consistent(const BasicJsonType& j)
|
||||
{
|
||||
bool ok = true;
|
||||
if (j.is_array() || j.is_object())
|
||||
{
|
||||
for (const auto& child : j)
|
||||
{
|
||||
ok = ok && child.m_parent == &j && parents_consistent(child);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE_TEMPLATE("Parent pointers stay consistent", J, json, ordered_json)
|
||||
{
|
||||
SECTION("parse, copy, move, assign, swap")
|
||||
{
|
||||
J j = J::parse(R"({"a":[1,{"b":[2,3]},[4]],"c":{"d":{"e":[]}}})");
|
||||
CHECK(parents_consistent(j));
|
||||
|
||||
J copy(j);
|
||||
CHECK(parents_consistent(copy));
|
||||
|
||||
J moved(std::move(copy));
|
||||
CHECK(parents_consistent(moved));
|
||||
|
||||
J assigned;
|
||||
assigned = j;
|
||||
CHECK(parents_consistent(assigned));
|
||||
|
||||
J other = {1, 2, {{"k", J::array({1, 2})}}};
|
||||
other.swap(assigned);
|
||||
CHECK(parents_consistent(other));
|
||||
CHECK(parents_consistent(assigned));
|
||||
}
|
||||
|
||||
SECTION("push_back growth")
|
||||
{
|
||||
J arr = J::array();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
arr.push_back(J::array({i}));
|
||||
}
|
||||
CHECK(parents_consistent(arr));
|
||||
|
||||
J obj = J::object();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
obj.push_back({std::to_string(i), J::array({i})});
|
||||
}
|
||||
CHECK(parents_consistent(obj));
|
||||
}
|
||||
|
||||
SECTION("emplace_back and emplace growth")
|
||||
{
|
||||
J arr = J::array();
|
||||
J obj = J::object();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
arr.emplace_back(J::array({i}));
|
||||
obj.emplace(std::to_string(i), J::array({i}));
|
||||
}
|
||||
CHECK(parents_consistent(arr));
|
||||
CHECK(parents_consistent(obj));
|
||||
}
|
||||
|
||||
SECTION("operator[] growth and at()")
|
||||
{
|
||||
J obj = J::object();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
obj[std::to_string(i)] = J::array({i});
|
||||
CHECK(obj[std::to_string(i / 2)].m_parent == &obj);
|
||||
}
|
||||
CHECK(parents_consistent(obj));
|
||||
|
||||
J arr = J::array();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
arr[static_cast<size_t>(i)] = J::object({{"x", i}});
|
||||
}
|
||||
for (size_t i = 0; i < 500; ++i)
|
||||
{
|
||||
CHECK(arr.at(i).m_parent == &arr);
|
||||
}
|
||||
CHECK(obj.at("3").m_parent == &obj);
|
||||
CHECK(parents_consistent(arr));
|
||||
CHECK(parents_consistent(obj));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("update() and merge_patch() keep parent pointers and diagnostic paths", J, json, ordered_json)
|
||||
{
|
||||
// enough keys to make the vector behind ordered_json reallocate several times
|
||||
const auto make_object = [](int first, int last)
|
||||
{
|
||||
J o = J::object();
|
||||
for (int i = first; i < last; ++i)
|
||||
{
|
||||
o[std::to_string(i)] = J::object({{"x", J::array({i})}});
|
||||
}
|
||||
return o;
|
||||
};
|
||||
|
||||
// const access, so operator[] cannot repair the parent pointers
|
||||
const auto check_path = [](const J & j, const std::string & key)
|
||||
{
|
||||
const std::string expected = "[json.exception.type_error.304] (/" + key + "/x) cannot use at() with array";
|
||||
CHECK_THROWS_WITH_AS(j[key]["x"].at("y"), expected.c_str(), json::type_error);
|
||||
};
|
||||
|
||||
SECTION("update(const_reference)")
|
||||
{
|
||||
J j = make_object(0, 200);
|
||||
j.update(make_object(100, 400));
|
||||
CHECK(j.size() == 400);
|
||||
CHECK(parents_consistent(j));
|
||||
check_path(j, "0");
|
||||
check_path(j, "150");
|
||||
check_path(j, "399");
|
||||
}
|
||||
|
||||
SECTION("update(const_reference, merge_objects)")
|
||||
{
|
||||
J j = make_object(0, 200);
|
||||
j["50"]["y"] = 1;
|
||||
J other = make_object(100, 400);
|
||||
other["50"] = J::object({{"z", J::array({1})}});
|
||||
j.update(other, true);
|
||||
CHECK(j.size() == 400);
|
||||
CHECK(j["50"].size() == 3);
|
||||
CHECK(parents_consistent(j));
|
||||
check_path(j, "0");
|
||||
check_path(j, "399");
|
||||
}
|
||||
|
||||
SECTION("update(first, last)")
|
||||
{
|
||||
J j = make_object(0, 200);
|
||||
J other = make_object(100, 400);
|
||||
j.update(other.begin(), other.end());
|
||||
CHECK(j.size() == 400);
|
||||
CHECK(parents_consistent(j));
|
||||
check_path(j, "0");
|
||||
check_path(j, "399");
|
||||
|
||||
J k = make_object(0, 200);
|
||||
k.update(other.begin(), other.end(), true);
|
||||
CHECK(k.size() == 400);
|
||||
CHECK(parents_consistent(k));
|
||||
check_path(k, "399");
|
||||
}
|
||||
|
||||
SECTION("merge_patch()")
|
||||
{
|
||||
J j = make_object(0, 200);
|
||||
J patch = make_object(100, 400);
|
||||
patch["0"] = nullptr;
|
||||
j.merge_patch(patch);
|
||||
CHECK(j.size() == 399);
|
||||
CHECK_FALSE(j.contains("0"));
|
||||
CHECK(parents_consistent(j));
|
||||
check_path(j, "1");
|
||||
check_path(j, "399");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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());
|
||||
}
|
||||
|
||||
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()) == "]");
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -10,8 +10,6 @@
|
||||
|
||||
#define JSON_TESTS_PRIVATE
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using nlohmann::json;
|
||||
|
||||
TEST_CASE("iterators 1")
|
||||
@@ -1630,108 +1628,3 @@ TEST_CASE("iterators 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("reverse iteration and member access", Json, nlohmann::json, nlohmann::ordered_json)
|
||||
{
|
||||
SECTION("array: -- and rbegin/rend")
|
||||
{
|
||||
Json j = {1, 2, 3, 4};
|
||||
|
||||
auto it = j.end();
|
||||
std::vector<int> seen;
|
||||
while (it != j.begin())
|
||||
{
|
||||
--it;
|
||||
seen.push_back(it->template get<int>());
|
||||
}
|
||||
CHECK(seen == std::vector<int> {4, 3, 2, 1});
|
||||
|
||||
auto it2 = j.end();
|
||||
auto old = it2--;
|
||||
CHECK(old == j.end());
|
||||
CHECK(*it2 == 4);
|
||||
|
||||
seen.clear();
|
||||
for (auto r = j.rbegin(); r != j.rend(); ++r)
|
||||
{
|
||||
seen.push_back(r->template get<int>());
|
||||
}
|
||||
CHECK(seen == std::vector<int> {4, 3, 2, 1});
|
||||
|
||||
seen.clear();
|
||||
for (auto r = j.crbegin(); r != j.crend(); ++r)
|
||||
{
|
||||
seen.push_back(*r);
|
||||
}
|
||||
CHECK(seen == std::vector<int> {4, 3, 2, 1});
|
||||
}
|
||||
|
||||
SECTION("object: -- and rbegin/rend")
|
||||
{
|
||||
Json j = {{"a", 1}, {"b", 2}, {"c", 3}};
|
||||
// order of keys depends on the object type; use the forward order as reference
|
||||
std::vector<std::string> fwd;
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
{
|
||||
fwd.push_back(it.key());
|
||||
}
|
||||
std::vector<std::string> rev(fwd.rbegin(), fwd.rend());
|
||||
|
||||
std::vector<std::string> seen;
|
||||
auto it = j.end();
|
||||
while (it != j.begin())
|
||||
{
|
||||
--it;
|
||||
seen.push_back(it.key());
|
||||
}
|
||||
CHECK(seen == rev);
|
||||
|
||||
auto it2 = j.end();
|
||||
auto old = it2--;
|
||||
CHECK(old == j.end());
|
||||
CHECK(it2.key() == rev.front());
|
||||
|
||||
seen.clear();
|
||||
for (auto r = j.rbegin(); r != j.rend(); ++r)
|
||||
{
|
||||
seen.push_back(r.key());
|
||||
}
|
||||
CHECK(seen == rev);
|
||||
}
|
||||
|
||||
SECTION("object: operator-> reads and writes the mapped value")
|
||||
{
|
||||
Json j = {{"a", 1}, {"s", "str"}};
|
||||
auto it = j.find("s");
|
||||
REQUIRE(it != j.end());
|
||||
CHECK(it->is_string());
|
||||
CHECK(it->size() == 1);
|
||||
*it = Json::array({1, 2});
|
||||
CHECK(it->is_array());
|
||||
it->push_back(3);
|
||||
CHECK(j["s"].size() == 3);
|
||||
|
||||
const Json& cj = j;
|
||||
auto cit = cj.find("a");
|
||||
REQUIRE(cit != cj.end());
|
||||
CHECK(cit->is_number_integer());
|
||||
CHECK(cit->template get<int>() == 1);
|
||||
}
|
||||
|
||||
SECTION("primitives: -- and ->")
|
||||
{
|
||||
Json j = 42;
|
||||
auto it = j.end();
|
||||
--it;
|
||||
CHECK(it == j.begin());
|
||||
CHECK(it->template get<int>() == 42);
|
||||
}
|
||||
|
||||
SECTION("comparing iterators of different containers throws")
|
||||
{
|
||||
Json a = {1, 2};
|
||||
Json b = {1, 2};
|
||||
CHECK_THROWS_WITH_AS(a.begin() == b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator);
|
||||
CHECK_THROWS_WITH_AS(a.begin() < b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator);
|
||||
}
|
||||
}
|
||||
@@ -2066,40 +2066,3 @@ TEST_CASE("JSON patch - every operation on ordered_json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json, nlohmann::json, nlohmann::ordered_json)
|
||||
{
|
||||
const auto roundtrip = [](const Json & a, const Json & b)
|
||||
{
|
||||
CAPTURE(a.dump())
|
||||
CAPTURE(b.dump())
|
||||
const Json p = Json::diff(a, b);
|
||||
CHECK(a.patch(p) == b);
|
||||
CHECK(Json::diff(b, b).empty());
|
||||
};
|
||||
|
||||
SECTION("reordered keys")
|
||||
{
|
||||
const Json a = Json::parse(R"({"x":1,"y":2,"z":{"p":1,"q":2}})");
|
||||
const Json b = Json::parse(R"({"z":{"q":2,"p":1},"y":2,"x":1})");
|
||||
roundtrip(a, b);
|
||||
roundtrip(b, a);
|
||||
}
|
||||
|
||||
SECTION("nested add, remove and replace")
|
||||
{
|
||||
const Json a = Json::parse(R"({"a":[1,2,{"k":"v","gone":true}],"b":{"c":{"d":1}},"e":"s"})");
|
||||
const Json b = Json::parse(R"({"a":[1,3,{"k":"w","new":[]}],"b":{"c":{"d":[1,2]}},"f":null})");
|
||||
roundtrip(a, b);
|
||||
roundtrip(b, a);
|
||||
}
|
||||
|
||||
SECTION("arrays of different length and type changes")
|
||||
{
|
||||
roundtrip(Json::parse("[1,2,3,4,5]"), Json::parse("[1,3]"));
|
||||
roundtrip(Json::parse("[1]"), Json::parse("[1,[2],{\"a\":3}]"));
|
||||
roundtrip(Json::parse("[]"), Json::parse("{}"));
|
||||
roundtrip(Json::parse("{\"a\":[]}"), Json::parse("{\"a\":{}}"));
|
||||
roundtrip(Json(1), Json::parse("[1]"));
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,8 @@ using nlohmann::json;
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("JSON pointers")
|
||||
{
|
||||
@@ -1077,242 +1071,3 @@ TEST_CASE("flatten of structured values")
|
||||
R"({"/z":"s","/a~1b/~0":1,"/a~1b/k":null,"/a/0":1,"/a/1/c":null,"/a/1/b":null,"/a/2/0/x~0~1/0":true,"/a/2/0/x~0~1/1":null,"/a/2/0/w":2})");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("flatten of ordered_json")
|
||||
{
|
||||
using nlohmann::ordered_json;
|
||||
|
||||
SECTION("keeps the document order of the leaves")
|
||||
{
|
||||
const ordered_json j = ordered_json::parse(R"({
|
||||
"z": 1, "a": {"y": [true, null, {}], "b": "x", "": []},
|
||||
"m~n": 2.5, "o/p": {"k": {}}, "0": [[1], [2, 3]]
|
||||
})");
|
||||
|
||||
const ordered_json flat = j.flatten();
|
||||
std::vector<std::string> keys;
|
||||
for (const auto& el : flat.items())
|
||||
{
|
||||
keys.push_back(el.key());
|
||||
}
|
||||
const std::vector<std::string> expected =
|
||||
{
|
||||
"/z", "/a/y/0", "/a/y/1", "/a/y/2", "/a/b", "/a/", "/m~0n", "/o~1p/k", "/0/0/0", "/0/1/0", "/0/1/1"
|
||||
};
|
||||
CHECK(keys == expected);
|
||||
CHECK(flat.size() == expected.size());
|
||||
CHECK(flat["/m~0n"] == 2.5);
|
||||
CHECK(flat["/a/y/2"].is_null());
|
||||
}
|
||||
|
||||
SECTION("round-trips and agrees with json")
|
||||
{
|
||||
ordered_json j;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
j["k" + std::to_string(i)] = {{"v", i}, {"l", {i, i + 1, "s"}}};
|
||||
}
|
||||
const ordered_json flat = j.flatten();
|
||||
CHECK(flat.size() == 800);
|
||||
CHECK(flat.unflatten() == j);
|
||||
|
||||
// same entries as for a sorted object
|
||||
const nlohmann::json plain = nlohmann::json::parse(j.dump());
|
||||
CHECK(nlohmann::json::parse(flat.dump()) == plain.flatten());
|
||||
}
|
||||
|
||||
SECTION("primitive and empty root")
|
||||
{
|
||||
CHECK(ordered_json(1).flatten() == ordered_json({{"", 1}}));
|
||||
CHECK(ordered_json::array().flatten() == ordered_json({{"", nullptr}}));
|
||||
}
|
||||
}
|
||||
namespace
|
||||
{
|
||||
// reference implementation of the array index rules (RFC 6901, Sect. 4) based on strtoull
|
||||
enum class index_class
|
||||
{
|
||||
ok,
|
||||
leading_zero,
|
||||
not_a_number,
|
||||
unresolved,
|
||||
exceeds_size_type
|
||||
};
|
||||
|
||||
index_class classify_index(const std::string& s, std::size_t& idx)
|
||||
{
|
||||
if (s.size() > 1 && s[0] == '0')
|
||||
{
|
||||
return index_class::leading_zero;
|
||||
}
|
||||
if (!s.empty() && s != "-" && !(s[0] >= '0' && s[0] <= '9'))
|
||||
{
|
||||
return index_class::not_a_number;
|
||||
}
|
||||
char* end = nullptr;
|
||||
errno = 0;
|
||||
const unsigned long long res = std::strtoull(s.c_str(), &end, 10); // NOLINT(runtime/int)
|
||||
if (end == s.c_str() || errno == ERANGE || static_cast<std::size_t>(end - s.c_str()) != s.size())
|
||||
{
|
||||
return index_class::unresolved;
|
||||
}
|
||||
if (res >= static_cast<unsigned long long>((std::numeric_limits<std::size_t>::max)())) // NOLINT(runtime/int)
|
||||
{
|
||||
return index_class::exceeds_size_type;
|
||||
}
|
||||
idx = static_cast<std::size_t>(res);
|
||||
return index_class::ok;
|
||||
}
|
||||
|
||||
// the observable behavior of json_pointer::at() and contains() for an array index token
|
||||
void check_index_token(const std::string& token)
|
||||
{
|
||||
const json array = {10, 20, 30};
|
||||
const json::json_pointer ptr("/" + token);
|
||||
|
||||
std::size_t idx = 0;
|
||||
const auto cls = classify_index(token, idx);
|
||||
const bool exists = cls == index_class::ok && idx < array.size();
|
||||
|
||||
CAPTURE(token)
|
||||
CHECK(array.contains(ptr) == (exists && token != "-"));
|
||||
|
||||
if (token == "-")
|
||||
{
|
||||
CHECK_THROWS_AS(array.at(ptr), json::out_of_range&);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cls)
|
||||
{
|
||||
case index_class::leading_zero:
|
||||
CHECK_THROWS_AS(array.at(ptr), json::parse_error&);
|
||||
break;
|
||||
case index_class::not_a_number:
|
||||
CHECK_THROWS_AS(array.at(ptr), json::parse_error&);
|
||||
break;
|
||||
case index_class::unresolved:
|
||||
case index_class::exceeds_size_type:
|
||||
CHECK_THROWS_AS(array.at(ptr), json::out_of_range&);
|
||||
break;
|
||||
case index_class::ok:
|
||||
default:
|
||||
if (exists)
|
||||
{
|
||||
CHECK(array.at(ptr) == array[idx]);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK_THROWS_AS(array.at(ptr), json::out_of_range&);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("JSON pointer array index tokens match strtoull semantics")
|
||||
{
|
||||
SECTION("boundary values")
|
||||
{
|
||||
const std::string max_digits = std::to_string((std::numeric_limits<std::size_t>::max)());
|
||||
const std::vector<std::string> tokens =
|
||||
{
|
||||
"", "-", "0", "1", "2", "3", "00", "01", "007", "+1", "-1", " 1", "1 ", "1a", "a1", "0x1", "1e1", "1.0",
|
||||
"999999999", "1000000000", "99999999999999999", "999999999999999999", "1000000000000000000",
|
||||
"9999999999999999999", "18446744073709551614", max_digits, "18446744073709551616",
|
||||
"99999999999999999999999", "000000000000000000000"
|
||||
};
|
||||
for (const auto& token : tokens)
|
||||
{
|
||||
check_index_token(token);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("random tokens")
|
||||
{
|
||||
std::mt19937 gen(42);
|
||||
const std::string alphabet = "0123456789012345678901234567890123456789-+ ex";
|
||||
for (int i = 0; i < 5000; ++i)
|
||||
{
|
||||
std::string token(static_cast<std::size_t>(gen() % 23), '0');
|
||||
for (auto& c : token)
|
||||
{
|
||||
c = alphabet[gen() % alphabet.size()];
|
||||
}
|
||||
check_index_token(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON pointer operations on pointers with escaped tokens")
|
||||
{
|
||||
const std::vector<std::string> strings =
|
||||
{
|
||||
"", "/", "//", "/a", "/a/b", "/a~1b", "/~0", "/~01", "/~10", "/a~0~1/b/", "/0/1/2", "/ ", "/a/", "/~1/~0"
|
||||
};
|
||||
|
||||
for (const auto& str : strings)
|
||||
{
|
||||
CAPTURE(str)
|
||||
const json::json_pointer ptr(str);
|
||||
CHECK(ptr.to_string() == str);
|
||||
CHECK(ptr / "x" / "y" / std::size_t{3} == json::json_pointer(str + "/x/y/3"));
|
||||
CHECK(ptr.parent_pointer() == (str.empty() ? ptr : json::json_pointer(str.substr(0, str.rfind('/')))));
|
||||
|
||||
// comparison with a string, in place, and with every other pointer
|
||||
#if !defined(JSON_TEST_DEPRECATED_FUNCTIONS_DELETED)
|
||||
for (const auto& other : strings)
|
||||
{
|
||||
CAPTURE(other)
|
||||
CHECK((ptr == other) == (str == other));
|
||||
CHECK((other == ptr) == (str == other));
|
||||
CHECK((ptr != other) == (str != other));
|
||||
}
|
||||
CHECK_THROWS_AS(ptr == "x", json::parse_error&);
|
||||
CHECK_THROWS_AS(ptr == "/~", json::parse_error&);
|
||||
CHECK_THROWS_AS(ptr == "/b/~2", json::parse_error&);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON pointer lookups")
|
||||
{
|
||||
json j = {{"a", {{"b", {1, 2, 3}}}}, {"", 7}, {"~/", {{"x", nullptr}}}};
|
||||
const json& cj = j;
|
||||
|
||||
CHECK(cj[json::json_pointer("/a/b/2")] == 3);
|
||||
CHECK(cj.at(json::json_pointer("/a/b/1")) == 2);
|
||||
CHECK(cj.at(json::json_pointer("/")) == 7);
|
||||
CHECK(cj.contains(json::json_pointer("/~0~1/x")));
|
||||
CHECK(!cj.contains(json::json_pointer("/a/c")));
|
||||
CHECK(cj.value(json::json_pointer("/a/b/0"), 5) == 1);
|
||||
CHECK(cj.value(json::json_pointer("/a/b/3"), 5) == 5);
|
||||
CHECK(cj.value(json::json_pointer("/a/zz"), 5) == 5);
|
||||
|
||||
CHECK_THROWS_AS(cj.at(json::json_pointer("/a/c")), json::out_of_range&);
|
||||
CHECK_THROWS_AS(cj.at(json::json_pointer("/a/b/3")), json::out_of_range&);
|
||||
|
||||
// non-const access reuses existing entries and creates missing ones
|
||||
j[json::json_pointer("/a/b/1")] = 20;
|
||||
j[json::json_pointer("/n/m")] = 1;
|
||||
CHECK(j["a"]["b"] == json({1, 20, 3}));
|
||||
CHECK(j["n"]["m"] == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch with moves and copies")
|
||||
{
|
||||
json doc = {{"a", {{"b", {1, 2, 3}}}}, {"c", {{"d", "x"}}}};
|
||||
|
||||
CHECK(doc.patch(json::parse(R"([{"op":"move","from":"/a/b","path":"/c/e"}])")) ==
|
||||
json::parse(R"({"a":{},"c":{"d":"x","e":[1,2,3]}})"));
|
||||
CHECK(doc.patch(json::parse(R"([{"op":"copy","from":"/a/b","path":"/c/e"}])")) ==
|
||||
json::parse(R"({"a":{"b":[1,2,3]},"c":{"d":"x","e":[1,2,3]}})"));
|
||||
CHECK(doc.patch(json::parse(R"([{"op":"move","from":"/a/b/0","path":"/a/b/2"}])")) ==
|
||||
json::parse(R"({"a":{"b":[2,3,1]},"c":{"d":"x"}})"));
|
||||
|
||||
// a failing move leaves a copy-patched document untouched
|
||||
CHECK_THROWS_AS(doc.patch(json::parse(R"([{"op":"move","from":"","path":""}])")), json::out_of_range&);
|
||||
json inplace = doc;
|
||||
CHECK_THROWS_AS(inplace.patch_inplace(json::parse(R"([{"op":"move","from":"","path":""}])")), json::out_of_range&);
|
||||
CHECK(inplace == doc);
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <sstream>
|
||||
#include <streambuf>
|
||||
@@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf
|
||||
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
|
||||
std::string remaining(std::istream& is)
|
||||
{
|
||||
@@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
|
||||
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&);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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")
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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&);
|
||||
}
|
||||
}
|
||||
|
||||
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]})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +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
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
// The exceptions in hot core functions are constructed out of line. These tests pin the
|
||||
// exact what() texts of the sites that moved, so the refactoring cannot change them.
|
||||
|
||||
#define CHECK_MSG(expr, msg) CHECK_THROWS_WITH(expr, msg)
|
||||
|
||||
TEST_CASE("exception messages of out-of-line throw sites")
|
||||
{
|
||||
json arr = {1, 2, 3};
|
||||
json arr2 = {4, 5};
|
||||
json obj = {{"a", 1}, {"b", 2}};
|
||||
json obj2 = {{"c", 3}};
|
||||
json num = 1;
|
||||
|
||||
SECTION("iterators")
|
||||
{
|
||||
CHECK_MSG((void)(arr.begin() == arr2.begin()), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers");
|
||||
CHECK_MSG((void)(arr.begin() < arr2.begin()), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers");
|
||||
CHECK_MSG((void)(obj.begin() < obj.end()), "[json.exception.invalid_iterator.213] cannot compare order of object iterators");
|
||||
CHECK_MSG(obj.begin() += 1, "[json.exception.invalid_iterator.209] cannot use offsets with object iterators");
|
||||
CHECK_MSG((void)(obj.begin() - 1), "[json.exception.invalid_iterator.209] cannot use offsets with object iterators");
|
||||
CHECK_MSG((void)obj.begin()[0], "[json.exception.invalid_iterator.208] cannot use operator[] for object iterators");
|
||||
CHECK_MSG((void)arr.begin().key(), "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
|
||||
json null;
|
||||
CHECK_MSG((void)*null.end(), "[json.exception.invalid_iterator.214] cannot get value");
|
||||
}
|
||||
|
||||
SECTION("constructors")
|
||||
{
|
||||
CHECK_MSG(json(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.201] iterators are not compatible");
|
||||
CHECK_MSG(json(num.begin() + 1, num.end() + 1), "[json.exception.invalid_iterator.204] iterators out of range");
|
||||
CHECK_MSG(json(json::initializer_list_t{1, 2}, false, json::value_t::object), "[json.exception.type_error.301] cannot create object from initializer list");
|
||||
}
|
||||
|
||||
SECTION("get_ref and operator[]")
|
||||
{
|
||||
CHECK_MSG((void)num.get_ref<std::string&>(), "[json.exception.type_error.303] incompatible ReferenceType for get_ref, actual type is number");
|
||||
CHECK_MSG((void)num[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with number");
|
||||
}
|
||||
|
||||
SECTION("at")
|
||||
{
|
||||
const json carr = arr;
|
||||
const json cobj = obj;
|
||||
CHECK_MSG((void)arr.at(static_cast<json::size_type>(10)), "[json.exception.out_of_range.401] array index 10 is out of range");
|
||||
CHECK_MSG((void)carr.at(static_cast<json::size_type>(3)), "[json.exception.out_of_range.401] array index 3 is out of range");
|
||||
CHECK_MSG((void)num.at(static_cast<json::size_type>(0)), "[json.exception.type_error.304] cannot use at() with number");
|
||||
CHECK_MSG((void)obj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
|
||||
CHECK_MSG((void)cobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
|
||||
CHECK_MSG((void)obj.at(std::string("zz")), "[json.exception.out_of_range.403] key 'zz' not found");
|
||||
CHECK_MSG((void)num.at("a"), "[json.exception.type_error.304] cannot use at() with number");
|
||||
|
||||
nlohmann::ordered_json oobj = {{"a", 1}};
|
||||
const nlohmann::ordered_json coobj = oobj;
|
||||
CHECK_MSG((void)oobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
|
||||
CHECK_MSG((void)coobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
|
||||
CHECK_MSG((void)coobj.at(std::string("zz")), "[json.exception.out_of_range.403] key 'zz' not found");
|
||||
}
|
||||
|
||||
SECTION("erase")
|
||||
{
|
||||
CHECK_MSG(arr.erase(arr2.begin()), "[json.exception.invalid_iterator.202] iterator does not fit current value");
|
||||
CHECK_MSG(num.erase(num.end()), "[json.exception.invalid_iterator.205] iterator out of range");
|
||||
CHECK_MSG(arr.erase(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.203] iterators do not fit current value");
|
||||
CHECK_MSG(num.erase(num.begin() + 1, num.end()), "[json.exception.invalid_iterator.204] iterators out of range");
|
||||
CHECK_MSG(arr.erase(static_cast<json::size_type>(10)), "[json.exception.out_of_range.401] array index 10 is out of range");
|
||||
}
|
||||
|
||||
SECTION("insert")
|
||||
{
|
||||
CHECK_MSG(arr.insert(arr2.begin(), 1), "[json.exception.invalid_iterator.202] iterator does not fit current value");
|
||||
CHECK_MSG(arr.insert(arr2.begin(), 2, 1), "[json.exception.invalid_iterator.202] iterator does not fit current value");
|
||||
CHECK_MSG(arr.insert(arr2.begin(), arr2.begin(), arr2.end()), "[json.exception.invalid_iterator.202] iterator does not fit current value");
|
||||
CHECK_MSG(arr.insert(arr.begin(), arr.begin(), arr2.end()), "[json.exception.invalid_iterator.210] iterators do not fit");
|
||||
CHECK_MSG(arr.insert(arr.begin(), arr.begin(), arr.end()), "[json.exception.invalid_iterator.211] passed iterators may not belong to container");
|
||||
CHECK_MSG(arr.insert(arr.begin(), obj.begin(), obj.end()), "[json.exception.invalid_iterator.202] iterators first and last must point to arrays");
|
||||
CHECK_MSG(arr.insert(arr2.begin(), {1, 2}), "[json.exception.invalid_iterator.202] iterator does not fit current value");
|
||||
CHECK_MSG(obj.insert(obj2.begin(), arr.end()), "[json.exception.invalid_iterator.210] iterators do not fit");
|
||||
CHECK_MSG(obj.insert(arr.begin(), arr.end()), "[json.exception.invalid_iterator.202] iterators first and last must point to objects");
|
||||
CHECK_MSG(obj.update(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.210] iterators do not fit");
|
||||
}
|
||||
}
|
||||
@@ -1,257 +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
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <forward_list>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <valarray>
|
||||
#include <vector>
|
||||
|
||||
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1)
|
||||
#if __has_include(<filesystem>)
|
||||
#include <filesystem>
|
||||
#define TO_JSON_RVALUE_HAS_FS 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
// element type that counts how it is converted to JSON: a conversion from an
|
||||
// lvalue copies, a conversion from an rvalue moves
|
||||
struct tracked
|
||||
{
|
||||
std::string payload;
|
||||
bool moved_from = false;
|
||||
|
||||
explicit tracked(std::string p) : payload(std::move(p)) {}
|
||||
tracked(const tracked&) = default;
|
||||
tracked& operator=(const tracked&) = default;
|
||||
tracked(tracked&& other) noexcept : payload(std::move(other.payload)), moved_from(other.moved_from)
|
||||
{
|
||||
other.moved_from = true;
|
||||
}
|
||||
tracked& operator=(tracked&& other) noexcept
|
||||
{
|
||||
payload = std::move(other.payload);
|
||||
moved_from = other.moved_from;
|
||||
other.moved_from = true;
|
||||
return *this;
|
||||
}
|
||||
~tracked() = default;
|
||||
};
|
||||
|
||||
int tracked_copies = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage)
|
||||
int tracked_moves = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage)
|
||||
|
||||
void to_json(json& j, const tracked& t)
|
||||
{
|
||||
++tracked_copies;
|
||||
j = t.payload;
|
||||
}
|
||||
|
||||
void to_json(json& j, tracked&& t)
|
||||
{
|
||||
++tracked_moves;
|
||||
j = std::move(t.payload);
|
||||
t.moved_from = true;
|
||||
}
|
||||
|
||||
// user type with its own rvalue-aware serializer
|
||||
struct udt_container
|
||||
{
|
||||
std::vector<int> values;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
namespace nlohmann
|
||||
{
|
||||
template<>
|
||||
struct adl_serializer<udt_container>
|
||||
{
|
||||
static void to_json(json& j, const udt_container& c)
|
||||
{
|
||||
j = c.values;
|
||||
j.push_back("copied");
|
||||
}
|
||||
static void to_json(json& j, udt_container&& c)
|
||||
{
|
||||
j = std::move(c.values);
|
||||
j.push_back("moved");
|
||||
}
|
||||
};
|
||||
} // namespace nlohmann
|
||||
|
||||
TEST_CASE("to_json from rvalue containers")
|
||||
{
|
||||
const std::string long_a(64, 'a');
|
||||
const std::string long_b(64, 'b');
|
||||
|
||||
SECTION("vector<std::string>: elements are moved")
|
||||
{
|
||||
std::vector<std::string> v{long_a, long_b};
|
||||
const json j = std::move(v);
|
||||
CHECK(j == json::array({long_a, long_b}));
|
||||
for (const auto& s : v) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
|
||||
{
|
||||
CHECK(s.empty()); // long strings leave the source element empty when moved
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("vector<std::string>: lvalues are copied and left untouched")
|
||||
{
|
||||
const std::vector<std::string> v{long_a, long_b};
|
||||
const json j = v;
|
||||
CHECK(j == json::array({long_a, long_b}));
|
||||
CHECK(v[0] == long_a);
|
||||
CHECK(v[1] == long_b);
|
||||
|
||||
std::vector<std::string> v2{long_a, long_b};
|
||||
const json j2 = v2;
|
||||
CHECK(j2 == j);
|
||||
CHECK(v2[0] == long_a);
|
||||
CHECK(v2[1] == long_b);
|
||||
}
|
||||
|
||||
SECTION("tracked elements: rvalue moves, lvalue copies")
|
||||
{
|
||||
tracked_copies = 0;
|
||||
tracked_moves = 0;
|
||||
|
||||
std::vector<tracked> v;
|
||||
v.emplace_back("x");
|
||||
v.emplace_back("y");
|
||||
|
||||
const json copy = v;
|
||||
CHECK(copy == json::array({"x", "y"}));
|
||||
CHECK(tracked_copies == 2);
|
||||
CHECK(tracked_moves == 0);
|
||||
CHECK_FALSE(v[0].moved_from);
|
||||
|
||||
const json moved = std::move(v);
|
||||
CHECK(moved == copy);
|
||||
CHECK(tracked_copies == 2);
|
||||
CHECK(tracked_moves == 2);
|
||||
}
|
||||
|
||||
SECTION("const rvalue is copied")
|
||||
{
|
||||
tracked_copies = 0;
|
||||
tracked_moves = 0;
|
||||
const std::vector<tracked> v(2, tracked("z"));
|
||||
const json j = std::move(v); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
|
||||
CHECK(j == json::array({"z", "z"}));
|
||||
CHECK(tracked_copies == 2);
|
||||
CHECK(tracked_moves == 0);
|
||||
}
|
||||
|
||||
SECTION("other sequence containers")
|
||||
{
|
||||
CHECK(json(std::deque<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
|
||||
CHECK(json(std::list<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
|
||||
CHECK(json(std::forward_list<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
|
||||
CHECK(json(std::array<std::string, 2> {{long_a, long_b}}) == json::array({long_a, long_b}));
|
||||
CHECK(json(std::set<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
|
||||
CHECK(json(std::vector<bool> {true, false}) == json::array({true, false}));
|
||||
CHECK(json(std::vector<int> {1, 2, 3}) == json::array({1, 2, 3}));
|
||||
CHECK(json(std::vector<std::vector<std::string>> {{long_a}, {long_b}}) == json::array({json::array({long_a}), json::array({long_b})}));
|
||||
CHECK(json(std::vector<json> {1, "a"}) == json::array({1, "a"}));
|
||||
}
|
||||
|
||||
SECTION("valarray")
|
||||
{
|
||||
std::valarray<int> va(3);
|
||||
va[0] = 1;
|
||||
va[1] = 2;
|
||||
va[2] = 3;
|
||||
CHECK(json(std::move(va)) == json::array({1, 2, 3})); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
|
||||
CHECK(json(std::valarray<int>(2)) == json::array({0, 0}));
|
||||
}
|
||||
|
||||
SECTION("array_t and json arrays")
|
||||
{
|
||||
json::array_t arr{1, 2};
|
||||
CHECK(json(std::move(arr)) == json::array({1, 2}));
|
||||
json a = json::array({1, 2});
|
||||
const json b = std::move(a);
|
||||
CHECK(b == json::array({1, 2}));
|
||||
}
|
||||
|
||||
SECTION("object-like containers")
|
||||
{
|
||||
std::map<std::string, std::string> m{{"a", long_a}, {"b", long_b}};
|
||||
const json j = std::move(m);
|
||||
CHECK(j == json({{"a", long_a}, {"b", long_b}}));
|
||||
for (const auto& kv : m) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
|
||||
{
|
||||
CHECK(kv.second.empty()); // values moved, keys (const) copied
|
||||
}
|
||||
|
||||
const std::map<std::string, std::string> cm{{"a", long_a}};
|
||||
const json jc = cm;
|
||||
CHECK(jc["a"] == long_a);
|
||||
CHECK(cm.at("a") == long_a);
|
||||
|
||||
std::unordered_map<std::string, std::string> um{{"a", long_a}};
|
||||
CHECK(json(std::move(um)) == json({{"a", long_a}}));
|
||||
|
||||
std::multimap<std::string, int> mm{{"a", 1}};
|
||||
CHECK(json(std::move(mm)) == json({{"a", 1}}));
|
||||
|
||||
tracked_copies = 0;
|
||||
tracked_moves = 0;
|
||||
std::map<std::string, tracked> mt;
|
||||
mt.emplace("k", tracked("v"));
|
||||
const json jt = std::move(mt);
|
||||
CHECK(jt == json({{"k", "v"}}));
|
||||
CHECK(tracked_moves == 1);
|
||||
CHECK(tracked_copies == 0);
|
||||
|
||||
json::object_t obj{{"a", 1}};
|
||||
CHECK(json(std::move(obj)) == json({{"a", 1}}));
|
||||
}
|
||||
|
||||
SECTION("user type with its own to_json overloads")
|
||||
{
|
||||
udt_container c{{1, 2}};
|
||||
const json lv = c;
|
||||
CHECK(lv == json::array({1, 2, "copied"}));
|
||||
const json rv = std::move(c);
|
||||
CHECK(rv == json::array({1, 2, "moved"}));
|
||||
}
|
||||
|
||||
SECTION("assignment and implicit conversion")
|
||||
{
|
||||
std::vector<std::string> v{long_a};
|
||||
json j;
|
||||
j = std::move(v);
|
||||
CHECK(j == json::array({long_a}));
|
||||
json k = {std::vector<std::string>{long_b}};
|
||||
CHECK(k.size() == 1);
|
||||
}
|
||||
|
||||
#ifdef TO_JSON_RVALUE_HAS_FS
|
||||
SECTION("std::filesystem::path rvalue is still a string")
|
||||
{
|
||||
CHECK(json(std::filesystem::path("a/b")) == json("a/b"));
|
||||
#ifdef __cpp_lib_char8_t
|
||||
CHECK(json(std::u8string(u8"a/b")) == json("a/b"));
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in new issue
Block a user