Compare commits

...
Author SHA1 Message Date
Niels Lohmann 3aa3d87983 Use std::array for the integer size class table
clang-tidy (cppcoreguidelines-avoid-c-arrays) rejects the C-style array.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 19:34:12 +02:00
Niels Lohmann 5010011656 Write MessagePack integers without branching on their sign or size
Like CBOR before #5824, MessagePack integer arrays were no faster than in
3.12.0: the type switch (number_integer vs. number_unsigned), the sign
test, and the fixint/8/16/32/64-bit ladder mispredict on mixed data.

They now take the CBOR approach:

- number_integer and number_unsigned share one case of write_msgpack.
- The format comes from the size class of x, where x is the value itself
  if it is not negative and 2 * (-1 - n) + 1 for a negative n: n fits
  int 8/16/32 exactly if that fits uint 8/16/32, so one table lookup
  serves both signs. The fixint bound (127, or -32) is selected through a
  mask, and the marker is 0xCB + class for uint and 0xCF + class for int.
- Arrays are written by an out-of-line write_msgpack_array() with runs of
  integers encoded straight into space reserved from the sink.

The CBOR integer code is factored into helpers both formats share
(integer_bits, integer_size_class, encode_first_byte_and_tail,
write_array_elements). The new path picks the format from the low 64
bits, so number types wider than that keep the previous range checks.

The output is unchanged; the integer-array test now also checks
MessagePack against an independent encoder.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 14:26:24 +02:00
Niels Lohmann bbb33374d4 Write CBOR integers without branching on their sign or size
The sizes of a run of integers are hard to predict, and so is the type of
each element in an array that mixes negative (number_integer) and
non-negative (number_unsigned) values. Writing an integer branched on the
type, the sign, and the 1/2/4/8-byte size class, and the mispredictions
were the larger part of the time left on integer arrays.

- number_integer and number_unsigned share one case of write_cbor; the
  major type and the argument are selected with a sign mask.
- An integer's head is assembled without branches: the size class comes
  from a table indexed by count_leading_zeros(), and the first byte and
  the argument in network order are stored as one byte and one 8-byte
  word, of which only the head's length is kept. Heads of strings and
  containers keep the branching write_cbor_head(), as their lengths are
  mostly predictable.
- Arrays are written by write_cbor_array(), kept out of line so that the
  registers its loop needs are not saved on every call of write_cbor.
  Runs of integers are encoded straight into space reserved from the sink
  (the new reserve()/commit() of both sinks), so the write pointer stays
  in a register instead of being reloaded for every value.

The output is unchanged; the new test compares integer arrays at every
head-size boundary, in runs shorter and longer than one reservation and
broken up by other values, with an independent encoder through each sink.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 13:48:16 +02:00
Niels Lohmann e26e1ad312 Append to the output vector through a position-tracking sink
The vector-returning to_cbor(), to_msgpack(), ... wrote through
output_vector_sink, which appended each write with push_back() or
insert(). A number's 1 to 8 payload bytes went through vector::insert(),
which libc++ and libstdc++ both run as an out-of-line insert-anywhere
routine; on integer arrays that call cost more than encoding the number,
so the devirtualization of #5286 brought nothing there, and with GCC the
writer was slower than in 3.12.0.

output_vector_sink now keeps its own write position: it grows the vector
geometrically ahead of the position (resize(), using any capacity the
vector already has), copies each write there, and shrinks the vector to
the written length in its destructor, also when a write throws. Writes of
256 characters or more that do not fit are appended with insert(), so a
large string or binary value is copied once instead of zero-filled and
copied. The sink is move-only, as it owns that trim.

output_vector_adapter (the to_*(j, vector) overloads) appends with
push_back()/insert() again, so a caller holding the adapter never sees
the space kept ahead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 13:47:36 +02:00
Niels Lohmann 7461a199b5 Drop the class wrapper from the C++20 comparison operator signatures in the docs (#5814)
* Drop the class wrapper from the C++20 comparison operator signatures in the docs

The signature boxes of operator<=> and the C++20 half of operator== wrapped
the member declarations in an incomplete "class basic_json { ... };". The
trailing const already marks them as members, and no other page uses such a
wrapper.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Drop the class wrapper from the json_pointer comparison signatures too

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 20:06:14 +02:00
Niels Lohmann e839bbd1c0 Export client-facing headers from json.hpp for IWYU / include-cleaner, and check clients in CI (#5813)
* Mark json_fwd.hpp as exported by json.hpp for IWYU / include-cleaner

json_fwd.hpp is the only header declaring the nlohmann::json alias, so
include-what-you-use and clang-tidy's misc-include-cleaner attributed
the name to it and told users of <nlohmann/json.hpp> to include
json_fwd.hpp as well (or instead, which breaks compilation).

Fixes #5812

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Export the remaining client-facing headers from json.hpp and check clients with IWYU

Besides json_fwd.hpp, include analyzers also told clients of
<nlohmann/json.hpp> to include detail/json_ref.hpp (brace
initialization), json_literals.hpp (_json, _json_pointer),
adl_serializer.hpp, detail/macro_scope.hpp (NLOHMANN_DEFINE_*) and
detail/abi_macros.hpp. Mark those includes as exported, too.

ci_single_binaries only checked each header on its own, so it could not
notice this. It now also runs IWYU and clang-tidy's misc-include-cleaner
on small client programs in tests/iwyu_consumer, which fail on develop.
With the exports, json_literals.hpp no longer gets the json_fwd.hpp
suggestion that kept it out of the IWYU gate, so it is gated again.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 17:17:15 +02:00
Niels Lohmann aa9620fb15 Skip the indentation after a newline 8 bytes at a time (#5810)
Pretty-printed JSON indents every line with a run of spaces, which
skip_whitespace() read one character at a time. For contiguous input
(bulk_scan) the lexer now skips the spaces after a newline 8 bytes at a
time directly in the input buffer, including the leading spaces of the
last, partial word. This bypasses get(), which is safe because
bulk_scan implies the lazy position from the previous commit: nothing
is counted per character.

Compared to lazy positions alone, pretty-printed input (twitter,
citm_catalog with indent 2 and 4) parses up to 13% faster and accept()
gets up to 30% faster (GCC 13 and clang 18 on x86-64, Apple clang 21 on
arm64). Minified input is unchanged within noise.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 17:16:52 +02:00
Niels Lohmann f23830b65e Derive the lexer position lazily for seekable input (#5809)
* Derive the lexer position lazily for seekable input

The lexer counted the read position on every character: two increments
and a newline check per get(), all on members kept in memory. For
seekable input (pointers, strings, other random-access byte ranges) the
position can be derived instead: chars_read_total is the adapter's read
count (including reads at EOF) minus a pending unget, and line and
column are recomputed from the consumed input only when get_position()
is called, i.e. on parse errors. JSON_DIAGNOSTIC_POSITIONS uses the new
O(1) get_chars_read_total(). Stream, file, wide-string and user-defined
adapters keep the eager counters.

Parsing pretty-printed input gets 8-12% faster and accept() 12-27%
(canada, citm_catalog, twitter; clang 21 arm64 and GCC 13 x86-64).
Error messages, byte offsets, SAX positions and diagnostic positions are
unchanged (differential comparison against develop on 548k inputs).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Move the std::vector<std::byte> binary type tests out of unit-regression3

The #4804/#4209 tests instantiated a second basic_json specialization
(with_binary_t<std::vector<std::byte>>), including its parser and CBOR
reader. With clang 18, the C++20 object of unit-regression3.cpp had
32656 COFF sections on develop; the lazy position helpers pushed it to
32975, past what the MinGW linker of the Windows clang jobs handles
("relocation truncated to fit: IMAGE_REL_AMD64_REL32 against .rdata").
unit-custom-binary-type.cpp already instantiates the same type, so the
tests move there; unit-regression3.cpp drops to 27616 sections.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Report the parse errors of the position test via SAX

The test caught json::parse_error, so it aborted in the
ci_test_noexceptions build, where JSON_NOEXCEPTION turns the throw into
std::abort(). sax_parse() hands the error position and the exception
object to parse_error() without throwing, so the test now records them
there and runs in every configuration.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 17:16:52 +02:00
satelliteprogrammer 40a62947d3 support JSON Merge Patch (RFC 7396) diff creation (#4965)
* support JSON Merge Patch (RFC 7396) diff creation

The JSON merge patch document format describes the set of modifications
to a resource's content, that more closely mimics the syntax of the
resource being modified.

However, and in contrast to JSON Patch (RFC 6902), a JSON Merge Patch
cannot express certain modifications, e.g., changing an array element
at a specific index, or setting a specific object value to null.
The null value in a JSON Merge Patch is used to remove the key from the
object.

The diff algorithm is not part of the RFC 7396, but it was tested
against all examples provided, plus additional cases on how null values
are handled.

Signed-off-by: Luís Murta <luis@murta.dev>

* improve algorithm efficiency

Signed-off-by: Luís Murta <luis@murta.dev>

* fix inconsistent nulls and empty arrays

Also add CHECKs for the patch, documentation and run `make amalgamate`.

Signed-off-by: Luís Murta <luis@murta.dev>

* documentation cleanups

Signed-off-by: Luís Murta <luis@murta.dev>

* add an iterative fallback to merge_diff

Bound the recursion depth like diff() and merge_patch(), and test values
nested past the bound.

Signed-off-by: Luís Murta <luis@murta.dev>

* address the remaining review comments

Signed-off-by: Luís Murta <luis@murta.dev>

* move multi-line raw strings out of CHECK macros

GCC 4.8/5, ICPC and NVHPC mis-tokenize multi-line raw string literals
inside macro arguments, breaking the build or the parsed JSON.

Signed-off-by: Luís Murta <luis@murta.dev>

---------

Signed-off-by: Luís Murta <luis@murta.dev>
2026-10-12 09:33:43 +02:00
Niels Lohmann e8b747e4bc Re-amalgamate single_include after #5795 and #5806 (#5808)
#5795 and #5806 changed binary_writer.hpp but were merged without
updated amalgamated files, so the check_amalgamation run on develop
(c59ad9525) fails. This is the output of 'make amalgamate'; no source
files change.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-11 11:26:11 +02:00
33 changed files with 3746 additions and 422 deletions

No files matched your search

+19 -7
View File
@@ -610,9 +610,8 @@ foreach(SRC_FILE ${SRC_FILES})
add_executable(single_${RELATIVE_SRC_FILE} EXCLUDE_FROM_ALL ${PROJECT_BINARY_DIR}/src_single/${RELATIVE_SRC_FILE}.cpp) add_executable(single_${RELATIVE_SRC_FILE} EXCLUDE_FROM_ALL ${PROJECT_BINARY_DIR}/src_single/${RELATIVE_SRC_FILE}.cpp)
target_include_directories(single_${RELATIVE_SRC_FILE} PRIVATE ${PROJECT_SOURCE_DIR}/include) target_include_directories(single_${RELATIVE_SRC_FILE} PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_compile_features(single_${RELATIVE_SRC_FILE} PRIVATE cxx_std_11) target_compile_features(single_${RELATIVE_SRC_FILE} PRIVATE cxx_std_11)
if(RELATIVE_SRC_FILE STREQUAL "json" OR RELATIVE_SRC_FILE STREQUAL "json_literals") if(RELATIVE_SRC_FILE STREQUAL "json")
# see below: report the diagnostics of json.hpp and json_literals.hpp without --error, so they # see below: report the diagnostics of json.hpp without --error, so they do not fail the build
# do not fail the build
set_property(TARGET single_${RELATIVE_SRC_FILE} PROPERTY CXX_INCLUDE_WHAT_YOU_USE ${IWYU_TOOL} -Xiwyu --max_line_length=300) set_property(TARGET single_${RELATIVE_SRC_FILE} PROPERTY CXX_INCLUDE_WHAT_YOU_USE ${IWYU_TOOL} -Xiwyu --max_line_length=300)
else() else()
set_property(TARGET single_${RELATIVE_SRC_FILE} PROPERTY CXX_INCLUDE_WHAT_YOU_USE "${iwyu_path_and_options}") set_property(TARGET single_${RELATIVE_SRC_FILE} PROPERTY CXX_INCLUDE_WHAT_YOU_USE "${iwyu_path_and_options}")
@@ -626,18 +625,31 @@ foreach(SRC_FILE ${SRC_FILES})
# reporting its diagnostics (informational, via CXX_INCLUDE_WHAT_YOU_USE above) but exclude it # reporting its diagnostics (informational, via CXX_INCLUDE_WHAT_YOU_USE above) but exclude it
# from the hard gate below so a fresh IWYU/compiler combination does not fail this target on a # from the hard gate below so a fresh IWYU/compiler combination does not fail this target on a
# nondeterministic suggestion for a header that already re-exports everything on purpose. # nondeterministic suggestion for a header that already re-exports everything on purpose.
# json_literals.hpp and json.hpp include each other on purpose (json.hpp includes it at its end if(NOT RELATIVE_SRC_FILE STREQUAL "json")
# unless JSON_NO_AUTOMATIC_UDLS is defined), and IWYU, not following the cycle, suggests replacing
# json.hpp with json_fwd.hpp although the literals need the complete basic_json; exclude it, too.
if(NOT RELATIVE_SRC_FILE STREQUAL "json" AND NOT RELATIVE_SRC_FILE STREQUAL "json_literals")
list(APPEND single_binaries_tus src_single/${RELATIVE_SRC_FILE}.cpp) list(APPEND single_binaries_tus src_single/${RELATIVE_SRC_FILE}.cpp)
endif() endif()
endforeach() endforeach()
# The checks above look at each header on its own. Client code that includes only <nlohmann/json.hpp>
# must not be told by include analyzers to include another nlohmann header for what it uses
# (issue #5812): json.hpp marks those includes "IWYU pragma: export", and both IWYU and clang-tidy's
# misc-include-cleaner (disabled in .clang-tidy for the library itself) check these clients.
file(GLOB CONSUMER_FILES ${PROJECT_SOURCE_DIR}/tests/iwyu_consumer/*.cpp)
foreach(CONSUMER_FILE ${CONSUMER_FILES})
get_filename_component(CONSUMER_NAME "${CONSUMER_FILE}" NAME_WE)
add_executable(iwyu_consumer_${CONSUMER_NAME} EXCLUDE_FROM_ALL ${CONSUMER_FILE})
target_include_directories(iwyu_consumer_${CONSUMER_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_compile_features(iwyu_consumer_${CONSUMER_NAME} PRIVATE cxx_std_11)
list(APPEND single_binaries iwyu_consumer_${CONSUMER_NAME})
list(APPEND single_binaries_tus ${CONSUMER_FILE})
endforeach()
if(IWYU_TOOL_PY) if(IWYU_TOOL_PY)
add_custom_target(ci_single_binaries add_custom_target(ci_single_binaries
DEPENDS ${single_binaries} DEPENDS ${single_binaries}
COMMAND ${IWYU_TOOL_PY} -p ${PROJECT_BINARY_DIR} ${single_binaries_tus} -- ${iwyu_options} COMMAND ${IWYU_TOOL_PY} -p ${PROJECT_BINARY_DIR} ${single_binaries_tus} -- ${iwyu_options}
COMMAND ${CLANG_TIDY_TOOL} -p ${PROJECT_BINARY_DIR} --checks=-*,misc-include-cleaner --warnings-as-errors=* ${CONSUMER_FILES}
VERBATIM
COMMENT "Check if headers are self-contained" COMMENT "Check if headers are self-contained"
) )
else() else()
+1
View File
@@ -59,6 +59,7 @@ Currently, only `remove`, `add`, and `replace` operations are generated.
- [patch](patch.md) applies a JSON Patch - [patch](patch.md) applies a JSON Patch
- [patch_inplace](patch_inplace.md) applies a JSON Patch in place - [patch_inplace](patch_inplace.md) applies a JSON Patch in place
- [merge_patch](merge_patch.md) applies a JSON Merge Patch - [merge_patch](merge_patch.md) applies a JSON Merge Patch
- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch
## Version history ## Version history
+1
View File
@@ -285,6 +285,7 @@ Access to the JSON value
### JSON Merge Patch functions ### JSON Merge Patch functions
- [**merge_patch**](merge_patch.md) - applies a JSON Merge Patch - [**merge_patch**](merge_patch.md) - applies a JSON Merge Patch
- [**merge_diff**](merge_diff.md) (_static_) - creates a diff as a JSON Merge Patch
## Static functions ## Static functions
@@ -0,0 +1,79 @@
# <small>nlohmann::basic_json::</small>merge_diff
```cpp
static basic_json merge_diff(const basic_json& source,
const basic_json& target);
```
Creates a [JSON Merge Patch](https://tools.ietf.org/html/rfc7396) so that value `source` can be changed into the value
`target` by calling the [`merge_patch`](merge_patch.md) function. The patch contains only what differs: changed and
added members with their new values, and removed members with the value `#!json null`. Nested objects are compared
member by member; any other value that differs, including an array, is replaced as a whole.
For two JSON values `source` and `target`, where `target` contains no object member whose value is `#!json null`, the
following code always yields `#!cpp true`:
```cpp
basic_json patched = source;
patched.merge_patch(merge_diff(source, target));
patched == target;
```
## Parameters
`source` (in)
: JSON value to compare from
`target` (in)
: JSON value to compare against
## Return value
a JSON Merge Patch to convert the `source` to `target`
## Exception safety
Strong guarantee: `source` and `target` are never modified.
## Complexity
Linear in the total number of values (including nested ones) in `source` and `target`, times the cost of a key-based
object operation (looking up a key, or adding one to the patch): logarithmic for [`json`](../json.md), and linear for
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
`ordered_json`, diffing two objects with `n` and `m` members therefore takes O((n+m)²).
## Notes
- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`. As in RFC 7396, a
`#!json null` member of `target` is treated as absent: the patch removes the member from `source` instead of setting
it to `#!json null`. A member that is `#!json null` in both `source` and `target` is unchanged and left out of the
patch, so `source` keeps it.
- A merge patch cannot reorder object members, and [`merge_patch`](merge_patch.md) appends added members at the end.
For [`ordered_json`](../ordered_json.md), the patched value therefore has the same members as `target`, but not
necessarily in the same order, and may compare unequal to it. The patch lists changed and removed members in the
order of `source`, followed by added members in the order of `target`.
## Examples
??? example
The following code shows how a JSON Merge Patch is created as a diff for two JSON values.
```cpp
--8<-- "examples/merge_diff.cpp"
```
Output:
```json
--8<-- "examples/merge_diff.output"
```
## See also
- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
- [merge_patch](merge_patch.md) applies a JSON Merge Patch
- [diff](diff.md) creates a diff as a JSON Patch
## Version history
- Added in version 3.13.0.
@@ -67,6 +67,7 @@ returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was
- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) - [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
- [patch](patch.md) apply a JSON patch - [patch](patch.md) apply a JSON patch
- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch
## Version history ## Version history
@@ -11,12 +11,10 @@ template<typename ScalarType>
bool operator==(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) bool operator==(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2)
// since C++20 // since C++20
class basic_json { bool operator==(const_reference rhs) const noexcept; // (1)
bool operator==(const_reference rhs) const noexcept; // (1)
template<typename ScalarType> template<typename ScalarType>
bool operator==(ScalarType rhs) const noexcept(/* see below */); // (2) bool operator==(ScalarType rhs) const noexcept(/* see below */); // (2)
};
``` ```
1. Compares two JSON values for equality according to the following rules: 1. Compares two JSON values for equality according to the following rules:
@@ -2,12 +2,10 @@
```cpp ```cpp
// since C++20 // since C++20
class basic_json { std::partial_ordering operator<=>(const_reference rhs) const noexcept; // (1)
std::partial_ordering operator<=>(const_reference rhs) const noexcept; // (1)
template<typename ScalarType> template<typename ScalarType>
std::partial_ordering operator<=>(const ScalarType rhs) const noexcept(/* see below */); // (2) std::partial_ordering operator<=>(const ScalarType rhs) const noexcept(/* see below */); // (2)
};
``` ```
1. 3-way compares two JSON values producing a result of type `std::partial_ordering` according to the following rules: 1. 3-way compares two JSON values producing a result of type `std::partial_ordering` according to the following rules:
@@ -18,13 +18,11 @@ bool operator==(
const json_pointer<RefStringTypeRhs>& rhs); // (2) const json_pointer<RefStringTypeRhs>& rhs); // (2)
// since C++20 // since C++20
class json_pointer { template<typename RefStringTypeRhs>
template<typename RefStringTypeRhs> bool operator==(
bool operator==( const json_pointer<RefStringTypeRhs>& rhs) const noexcept; // (1)
const json_pointer<RefStringTypeRhs>& rhs) const noexcept; // (1)
bool operator==(const string_t& rhs) const; // (2) bool operator==(const string_t& rhs) const; // (2)
};
``` ```
1. Compares two JSON pointers for equality by comparing their reference tokens. 1. Compares two JSON pointers for equality by comparing their reference tokens.
@@ -2,10 +2,8 @@
```cpp ```cpp
// since C++20 // since C++20
class json_pointer { template<typename RefStringTypeRhs>
template<typename RefStringTypeRhs> std::strong_ordering operator<=>(const json_pointer<RefStringTypeRhs>& rhs) const noexcept; // *NOPAD*
std::strong_ordering operator<=>(const json_pointer<RefStringTypeRhs>& rhs) const noexcept; // *NOPAD*
};
``` ```
3-way compares two JSON pointers by lexicographically comparing their sequences of reference tokens: corresponding 3-way compares two JSON pointers by lexicographically comparing their sequences of reference tokens: corresponding
+51
View File
@@ -0,0 +1,51 @@
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace nlohmann::literals;
int main()
{
// the source document
json source = R"(
{
"title": "Goodbye!",
"author": {
"givenName": "John",
"familyName": "Doe"
},
"tags": [
"example",
"sample"
],
"content": "This will be unchanged"
}
)"_json;
// the target document
json target = R"(
{
"title": "Hello!",
"author": {
"givenName": "John"
},
"tags": [
"example"
],
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890"
}
)"_json;
// create the patch
json patch = json::merge_diff(source, target);
// roundtrip
json patched_source = source;
patched_source.merge_patch(patch);
// output patch and roundtrip result
std::cout << std::setw(4) << patch << "\n\n"
<< std::setw(4) << patched_source << std::endl;
}
@@ -0,0 +1,22 @@
{
"author": {
"familyName": null
},
"phoneNumber": "+01-123-456-7890",
"tags": [
"example"
],
"title": "Hello!"
}
{
"author": {
"givenName": "John"
},
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890",
"tags": [
"example"
],
"title": "Hello!"
}
+4
View File
@@ -9,6 +9,9 @@ syntax that closely mimics the document being modified. Unlike [JSON Patch](json
express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is
easier to read and write for object-shaped documents. easier to read and write for object-shaped documents.
The function [`merge_diff`](../api/basic_json/merge_diff.md) creates a merge patch that changes one JSON value into
another.
!!! tip "Not a general deep merge" !!! tip "Not a general deep merge"
A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target. A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target.
@@ -35,4 +38,5 @@ easier to read and write for object-shaped documents.
- [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations - [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations
- [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch - [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch
- Function [`merge_patch`](../api/basic_json/merge_patch.md) - Function [`merge_patch`](../api/basic_json/merge_patch.md)
- Function [`merge_diff`](../api/basic_json/merge_diff.md)
- Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively - Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively
+4 -2
View File
@@ -185,9 +185,11 @@ The lexer detects two optional extensions at compile time. Only `iterator_input_
random-access input of single-byte characters: random-access input of single-byte characters:
- `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input - `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input
for error messages instead of copying every character it reads. for error messages instead of copying every character it reads. With `get_read_count()` and
`count_consumed_lines()`, the lexer also derives the read position from the adapter and counts lines only when an
error needs them, instead of counting characters and lines on every read.
- `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in - `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in
contiguous memory, several bytes at a time. contiguous memory, several bytes at a time, and skip the spaces that indent a line in 8-byte steps.
The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the
`from_*` functions: `from_*` functions:
+1
View File
@@ -182,6 +182,7 @@ nav:
- 'json_serializer': api/basic_json/json_serializer.md - 'json_serializer': api/basic_json/json_serializer.md
- 'max_size': api/basic_json/max_size.md - 'max_size': api/basic_json/max_size.md
- 'meta': api/basic_json/meta.md - 'meta': api/basic_json/meta.md
- 'merge_diff': api/basic_json/merge_diff.md
- 'merge_patch': api/basic_json/merge_patch.md - 'merge_patch': api/basic_json/merge_patch.md
- 'number_float_t': api/basic_json/number_float_t.md - 'number_float_t': api/basic_json/number_float_t.md
- 'number_integer_t': api/basic_json/number_integer_t.md - 'number_integer_t': api/basic_json/number_integer_t.md
+19
View File
@@ -42,6 +42,25 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#endif #endif
} }
/// number of trailing zero bits of x (x != 0)
inline int count_trailing_zeros(std::uint64_t x) noexcept
{
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#else
int n = 0;
for (int shift = 32; shift != 0; shift >>= 1)
{
if ((x << (64 - shift)) == 0)
{
n += shift;
x >>= shift;
}
}
return n;
#endif
}
/// the 128-bit product of two 64-bit numbers /// the 128-bit product of two 64-bit numbers
struct uint128_parts struct uint128_parts
{ {
@@ -8,12 +8,12 @@
#pragma once #pragma once
#include <algorithm> // min #include <algorithm> // count, find, min
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstring> // strlen #include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next #include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <streambuf> // streambuf #include <streambuf> // streambuf
#include <string> // string, char_traits #include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer #include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
@@ -297,6 +297,7 @@ class iterator_input_adapter
return result; return result;
} }
++eof_reads;
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
@@ -306,6 +307,26 @@ class iterator_input_adapter
return static_cast<std::size_t>(std::distance(begin, current)); return static_cast<std::size_t>(std::distance(begin, current));
} }
// number of get_character() calls so far, including those that returned
// EOF; the lexer derives its position from it (see lexer::lazy_position)
std::size_t get_read_count() const
{
return get_consumed_count() + eof_reads;
}
// number of newlines in the consumed range [0, last_index); @a line_start
// is set to the index after the last of them (0 if there is none); only
// valid when supports_seek
std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
{
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
const auto newline = char_traits<char_type>::to_char_type('\n');
using reverse_it = std::reverse_iterator<IteratorType>;
const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
}
// append the already-consumed characters in the half-open range // append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek // [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType> template<typename ContainerType>
@@ -422,6 +443,8 @@ class iterator_input_adapter
IteratorType begin; IteratorType begin;
IteratorType current; IteratorType current;
SentinelType end; SentinelType end;
/// number of get_character() calls that returned EOF
std::size_t eof_reads = 0;
template<typename BaseInputAdapter, size_t T> template<typename BaseInputAdapter, size_t T>
friend struct wide_string_input_helper; friend struct wide_string_input_helper;
+3 -3
View File
@@ -217,7 +217,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
v.start_position = lexer->get_position() - 1; v.start_position = lexer->get_chars_read_total() - 1;
} }
} }
@@ -231,7 +231,7 @@ struct diagnostic_positions
if (lexer) if (lexer)
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
} }
} }
@@ -247,7 +247,7 @@ struct diagnostic_positions
// Lexer has read past the current field value, so set the end position to the current position. // Lexer has read past the current field value, so set the end position to the current position.
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
switch (v.type()) switch (v.type())
{ {
+138 -20
View File
@@ -17,6 +17,7 @@
#include <utility> // move #include <utility> // move
#include <vector> // vector #include <vector> // vector
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/input/input_adapters.hpp> #include <nlohmann/detail/input/input_adapters.hpp>
#include <nlohmann/detail/input/number_parse.hpp> #include <nlohmann/detail/input/number_parse.hpp>
#include <nlohmann/detail/input/position_t.hpp> #include <nlohmann/detail/input/position_t.hpp>
@@ -186,6 +187,15 @@ class lexer : public lexer_base<BasicJsonType>
static constexpr bool lazy_token_string = static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {}); input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// 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
/// chars_read_total is the adapter's read count (minus a pending unget),
/// 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;
/// whether a simulated unget can be passed on to the input adapter, which /// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see /// then leaves the character in the input; see
/// input_adapter_supports_lookahead /// input_adapter_supports_lookahead
@@ -195,7 +205,9 @@ class lexer : public lexer_base<BasicJsonType>
/// whether string scanning may bulk-consume runs of ordinary characters /// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires /// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing /// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. /// 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.
static constexpr bool bulk_scan = static constexpr bool bulk_scan =
lazy_token_string 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> {});
@@ -331,10 +343,6 @@ class lexer : public lexer_base<BasicJsonType>
} }
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(pos); ia.bulk_skip(pos);
// the run contains no newline (all bytes < 0x20 are treated as special),
// so only the flat character counters advance
position.chars_read_total += pos;
position.chars_read_current_line += pos;
} }
/// streaming input: no bulk fast path /// streaming input: no bulk fast path
@@ -1567,8 +1575,6 @@ scan_number_done:
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{ {
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return integer_result; return integer_result;
} }
// The value does not fit an integer, so this token converts as a // The value does not fit an integer, so this token converts as a
@@ -1585,8 +1591,6 @@ scan_number_done:
decimal_point_position = dot_index; decimal_point_position = dot_index;
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return convert_number(number_type, mantissa_end); return convert_number(number_type, mantissa_end);
} }
@@ -1637,7 +1641,7 @@ scan_number_done:
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
// the first character of the token has already been read, hence the -1 // the first character of the token has already been read, hence the -1
token_start_position = position.chars_read_total - 1; token_start_position = get_chars_read_total() - 1;
#endif #endif
note_token_start(std::integral_constant<bool, lazy_token_string> {}); note_token_start(std::integral_constant<bool, lazy_token_string> {});
@@ -1690,6 +1694,15 @@ scan_number_done:
/// per-character position counters (line-count-on-'\n' bookkeeping is /// per-character position counters (line-count-on-'\n' bookkeeping is
/// handled afterwards, in track_after_read(), once `current` is known) /// handled afterwards, in track_after_read(), once `current` is known)
void advance_position() noexcept void advance_position() noexcept
{
advance_position_impl(std::integral_constant<bool, lazy_position> {});
}
/// seekable adapter: the position is derived from the adapter on demand
void advance_position_impl(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the character
void advance_position_impl(std::false_type /*lazy*/) noexcept
{ {
++position.chars_read_total; ++position.chars_read_total;
++position.chars_read_current_line; ++position.chars_read_current_line;
@@ -1703,7 +1716,16 @@ scan_number_done:
// seekable adapters reconstruct the token lazily on error (see // seekable adapters reconstruct the token lazily on error (see
// get_token_string), so the eager per-character copy is skipped // get_token_string), so the eager per-character copy is skipped
capture_char(std::integral_constant<bool, lazy_token_string> {}); capture_char(std::integral_constant<bool, lazy_token_string> {});
track_newline(std::integral_constant<bool, lazy_position> {});
return current;
}
/// seekable adapter: lines are counted on demand in get_position()
void track_newline(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: start a new line after reading a newline
void track_newline(std::false_type /*lazy*/) noexcept
{
if (current == '\n') if (current == '\n')
{ {
++position.lines_read; ++position.lines_read;
@@ -1712,8 +1734,6 @@ scan_number_done:
chars_read_before_newline = position.chars_read_current_line; chars_read_before_newline = position.chars_read_current_line;
position.chars_read_current_line = 0; position.chars_read_current_line = 0;
} }
return current;
} }
/*! /*!
@@ -1757,12 +1777,22 @@ scan_number_done:
We implement unget by setting variable next_unget to true. The input is not We implement unget by setting variable next_unget to true. The input is not
changed - we just simulate ungetting by modifying chars_read_total, changed - we just simulate ungetting by modifying chars_read_total,
chars_read_current_line, and token_string. The next call to get() will chars_read_current_line, and token_string. The next call to get() will
behave as if the unget character is read again. behave as if the unget character is read again. With lazy_position, the
pending next_unget is all get_chars_read_total() needs.
*/ */
void unget() void unget()
{ {
next_unget = true; next_unget = true;
unget_position(std::integral_constant<bool, lazy_position> {});
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: get_chars_read_total() accounts for next_unget
void unget_position(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: step the counters back over the ungotten character
void unget_position(std::false_type /*lazy*/) noexcept
{
--position.chars_read_total; --position.chars_read_total;
// in case we "unget" a newline, we have to also decrement the lines_read // in case we "unget" a newline, we have to also decrement the lines_read
@@ -1785,8 +1815,6 @@ scan_number_done:
{ {
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
} }
/// adapter without lookahead: nothing to do (see release_lookahead) /// adapter without lookahead: nothing to do (see release_lookahead)
@@ -1857,10 +1885,21 @@ scan_number_done:
// diagnostics // diagnostics
///////////////////// /////////////////////
/// return position of last read token /*!
constexpr position_t get_position() const noexcept @brief return the number of characters read so far
Unlike get_position(), this is O(1) for all adapters, so it may be called
for every value (JSON_DIAGNOSTIC_POSITIONS).
*/
std::size_t get_chars_read_total() const noexcept
{ {
return position; return get_chars_read_total_impl(std::integral_constant<bool, lazy_position> {});
}
/// return position of last read token
position_t get_position() const
{
return get_position_impl(std::integral_constant<bool, lazy_position> {});
} }
/*! /*!
@@ -1897,6 +1936,43 @@ scan_number_done:
} }
#endif #endif
private:
/// seekable adapter: every get() either read from the adapter (including
/// reads at EOF, which get() counts as characters as well) or replayed an
/// ungotten character, so the count is the adapter's read count minus a
/// still pending unget
std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept
{
return ia.get_read_count() - (next_unget ? 1u : 0u);
}
/// streaming adapter: the counter is maintained by get() and unget()
std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept
{
return position.chars_read_total;
}
/// seekable adapter: count the lines in the consumed input; O(input read
/// so far), but only called on errors
position_t get_position_impl(std::true_type /*lazy*/) const
{
position_t result;
result.chars_read_total = get_chars_read_total();
// reads at EOF count as characters but are not part of the input
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
std::size_t line_start = 0;
result.lines_read = ia.count_consumed_lines(consumed, line_start);
result.chars_read_current_line = result.chars_read_total - line_start;
return result;
}
/// streaming adapter: the position is maintained by get() and unget()
position_t get_position_impl(std::false_type /*lazy*/) const noexcept
{
return position;
}
public:
/// seekable adapter: rebuild the last read token from the input on demand /// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{ {
@@ -2004,15 +2080,57 @@ scan_number_done:
// into one that reloads the pointer from memory every character // into one that reloads the pointer from memory every character
do do
{ {
if (current == '\n')
{
skip_indentation(std::integral_constant<bool, bulk_scan> {});
}
get_ignoring_pending_unget(); get_ignoring_pending_unget();
} }
while (current_is_whitespace()); while (current_is_whitespace());
} }
/*!
@brief contiguous input: skip the space indentation that follows a newline
8 bytes at a time
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.
*/
void skip_indentation(std::true_type /*bulk*/)
{
JSON_ASSERT(!next_unget);
const std::size_t remaining = ia.bulk_remaining();
if (remaining < 8)
{
return;
}
const char* const data = reinterpret_cast<const char*>(ia.bulk_data());
std::size_t pos = 0;
for (; pos + 8 <= remaining; pos += 8)
{
// little-endian, so the first byte is the lowest
const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull;
if (non_space != 0)
{
// skip the spaces before the first other byte of this word
pos += static_cast<std::size_t>(count_trailing_zeros(non_space)) / 8;
break;
}
}
ia.bulk_skip(pos);
}
/// streaming input: no bulk fast path
void skip_indentation(std::false_type /*bulk*/) const noexcept {}
token_type scan() token_type scan()
{ {
// initially, skip the BOM // initially, skip the BOM
if (position.chars_read_total == 0 && !skip_bom()) if (get_chars_read_total() == 0 && !skip_bom())
{ {
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
return token_type::parse_error; return token_type::parse_error;
@@ -2039,7 +2157,7 @@ scan_number_done:
token_type scan_expecting(token_type expected_type) token_type scan_expecting(token_type expected_type)
{ {
JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator);
JSON_ASSERT(position.chars_read_total > 0); JSON_ASSERT(get_chars_read_total() > 0);
const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ','); const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ',');
skip_whitespace(); skip_whitespace();
if (JSON_HEDLEY_LIKELY(current == expected_char)) if (JSON_HEDLEY_LIKELY(current == expected_char))
+5 -3
View File
@@ -370,7 +370,7 @@ class parser
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON"); return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON");
} }
@@ -520,8 +520,10 @@ class parser
{ {
// MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static
static_cast<void>(sax); static_cast<void>(sax);
return sax.parse_error(m_lexer.get_position(), m_lexer.get_token_string(), // computing the position is O(input read so far) for seekable input
parse_error::create(exception_id::syntax_error, m_lexer.get_position(), message, nullptr)); const position_t position = m_lexer.get_position();
return sax.parse_error(position, m_lexer.get_token_string(),
parse_error::create(exception_id::syntax_error, position, message, nullptr));
} }
std::string exception_message(const token_type expected, const std::string& context) std::string exception_message(const token_type expected, const std::string& context)
+321 -76
View File
@@ -23,6 +23,7 @@
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64 #include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
#endif #endif
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/input/binary_reader.hpp> #include <nlohmann/detail/input/binary_reader.hpp>
#include <nlohmann/detail/input/string_scan.hpp> #include <nlohmann/detail/input/string_scan.hpp>
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
@@ -204,24 +205,12 @@ class binary_writer
} }
case value_t::number_integer: case value_t::number_integer:
{
if (j.m_data.m_value.number_integer >= 0)
{
// CBOR does not differentiate between positive signed
// integers and unsigned integers
write_cbor_head(0x00, static_cast<std::uint64_t>(j.m_data.m_value.number_integer));
}
else
{
// a negative integer n is encoded as -1 - n
write_cbor_head(0x20, static_cast<std::uint64_t>(-1 - j.m_data.m_value.number_integer));
}
break;
}
case value_t::number_unsigned: case value_t::number_unsigned:
{ {
write_cbor_head(0x00, j.m_data.m_value.number_unsigned); // one case for both, so that an array mixing negative
// (number_integer) and non-negative (number_unsigned) values
// does not mispredict the switch
write_cbor_integer(j);
break; break;
} }
@@ -256,13 +245,7 @@ class binary_writer
case value_t::array: case value_t::array:
{ {
// step 1: write control byte and the array size write_cbor_array(j, depth);
write_cbor_head(0x80, j.m_data.m_value.array->size());
for (const auto& el : *j.m_data.m_value.array)
{
write_cbor(el, depth + 1);
}
break; break;
} }
@@ -420,54 +403,12 @@ class binary_writer
} }
case value_t::number_integer: case value_t::number_integer:
{
if (j.m_data.m_value.number_integer >= 0)
{
// MessagePack does not differentiate between positive
// signed integers and unsigned integers.
write_msgpack_unsigned(static_cast<std::uint64_t>(j.m_data.m_value.number_integer));
}
else
{
if (j.m_data.m_value.number_integer >= -32)
{
// negative fixnum
write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer));
}
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int8_t>::min)() &&
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int8_t>::max)())
{
// int 8
oa.write_character(to_char_type(0xD0));
write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer));
}
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int16_t>::min)() &&
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int16_t>::max)())
{
// int 16
oa.write_character(to_char_type(0xD1));
write_number(static_cast<std::int16_t>(j.m_data.m_value.number_integer));
}
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int32_t>::min)() &&
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int32_t>::max)())
{
// int 32
oa.write_character(to_char_type(0xD2));
write_number(static_cast<std::int32_t>(j.m_data.m_value.number_integer));
}
else
{
// int 64
oa.write_character(to_char_type(0xD3));
write_number(static_cast<std::int64_t>(j.m_data.m_value.number_integer));
}
}
break;
}
case value_t::number_unsigned: case value_t::number_unsigned:
{ {
write_msgpack_unsigned(static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned)); // one case for both, so that an array mixing negative
// (number_integer) and non-negative (number_unsigned) values
// does not mispredict the switch
write_msgpack_integer(j, std::integral_constant<bool, msgpack_integers_fit_64_bits()>());
break; break;
} }
@@ -485,13 +426,7 @@ class binary_writer
case value_t::array: case value_t::array:
{ {
// step 1: write control byte and the array size write_msgpack_array(j, depth);
write_msgpack_array_prefix(j.m_data.m_value.array->size(), j);
for (const auto& el : *j.m_data.m_value.array)
{
write_msgpack(el, depth + 1);
}
break; break;
} }
@@ -1951,6 +1886,316 @@ class binary_writer
} }
} }
/*!
@brief size class of an unsigned integer from its bit length
1, 2, 3, or 4 if @a x fits 1, 2, 4, or 8 bytes. Looked up from the number
of leading zero bits instead of found by branches: the sizes of a stream
of numbers are hard to predict, and a mispredicted branch costs more than
writing the number. (A table, as comparisons are vectorized by clang into
slower code.)
*/
static unsigned integer_size_class(const std::uint64_t x) noexcept
{
static constexpr std::array<std::uint8_t, 64> size_class_by_leading_zeros =
{
{
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
2, 2, 2, 2, 2, 2, 2, 2,
1, 1, 1, 1, 1, 1, 1, 1
}
};
// x | 1 keeps count_leading_zeros() away from 0
return size_class_by_leading_zeros[static_cast<std::size_t>(count_leading_zeros(x | 1u))];
}
/*!
@brief encode @a first_byte and the low-order bytes of @a value that
@a size_class calls for (none, or 1, 2, 4, or 8) in network order
to @a out
@return the number of characters of the encoding; up to
@ref integer_encoding_space characters at @a out are overwritten
The bytes are written as one byte and one 8-byte word whose bytes after
the value are scratch that the caller drops, so that writing them does not
depend on the size class. For class 0 the word is all scratch; it is
shifted by 60 bits rather than 64, which would be undefined.
*/
std::size_t encode_first_byte_and_tail(CharType* out, const std::uint8_t first_byte, const unsigned size_class, const std::uint64_t value) const noexcept
{
out[0] = to_char_type(first_byte);
const unsigned unused_bits = 64u - ((8u << size_class) >> 1u);
const std::uint64_t tail = is_little_endian ? byte_swap(value) >> unused_bits : value << unused_bits;
std::memcpy(out + 1, &tail, sizeof(tail));
return ((std::size_t{1} << size_class) >> 1u) + 1;
}
/// characters encode_first_byte_and_tail() may overwrite: the first byte
/// and one 8-byte word
static constexpr std::size_t integer_encoding_space = 9;
/*!
@brief the 64 bits of a number_integer or number_unsigned value, and a mask
that is all ones if it is negative, else zero
Selected through masks rather than branches, as the type and the sign of
the values in an array of mixed signs alternate unpredictably (non-negative
integers are number_unsigned after parsing). Up to 64 bits, the sign is bit
63 of the result (a narrower integer is sign-extended); computing it with
masks keeps GCC from branching on the type. A wider integer has to be
compared.
*/
static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept
{
const bool is_signed = j.m_data.m_type == value_t::number_integer;
const std::uint64_t raw = is_signed
? static_cast<std::uint64_t>(j.m_data.m_value.number_integer)
: static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned);
sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t)
? (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u))
: std::uint64_t{0} - static_cast<std::uint64_t>(is_signed && j.m_data.m_value.number_integer < 0);
return raw;
}
/*!
@brief encode the head of a CBOR integer to @a out without branching on its
size
@return the number of characters of the head
Same output as @ref write_cbor_head. The lengths of strings and containers
are mostly predictable, so their heads keep the branches; the sizes of a
run of integers are not, so an integer's head is assembled without them.
*/
std::size_t encode_cbor_integer_head(CharType* out, const std::uint8_t major_type, const std::uint64_t argument) const noexcept
{
// an argument of at most 23 is in size class 1 (one byte) and is moved
// to class 0, the first byte, by subtracting 1
const auto is_immediate = static_cast<unsigned>(argument <= 0x17u);
const unsigned size_class = integer_size_class(argument) - is_immediate;
// the first byte: the argument itself if it is immediate, else
// 0x17 + size_class, selected through a mask
const std::uint64_t immediate_mask = std::uint64_t{0} - is_immediate;
const auto first_byte = static_cast<std::uint8_t>(major_type + ((argument & immediate_mask) | ((0x17u + size_class) & ~immediate_mask)));
return encode_first_byte_and_tail(out, first_byte, size_class, argument);
}
/*!
@brief write an array value (head and elements) as CBOR
Integer elements, the most common scalar, are written without a call to
@ref write_cbor. The loop is kept out of line: inlined into write_cbor, its
registers would be saved and restored on every call of write_cbor, which
is one call per value nested in an object.
*/
JSON_HEDLEY_NEVER_INLINE
void write_cbor_array(const BasicJsonType& j, const std::size_t depth)
{
write_cbor_head(0x80, j.m_data.m_value.array->size());
write_array_elements(j, [this](CharType * out, const BasicJsonType & el)
{
return encode_cbor_integer(out, el);
}, [this, depth](const BasicJsonType & el)
{
write_cbor(el, depth + 1);
});
}
/*!
@brief write the elements of the array @a j, runs of integers with
@a encode_integer and all other values with @a write_value
Runs of integers are encoded straight into space reserved from the sink
for a whole run: the write pointer stays a local, where it would otherwise
be reloaded from the sink for every value, as a store of a character may
alias it.
*/
template<typename EncodeInteger, typename WriteValue>
void write_array_elements(const BasicJsonType& j, EncodeInteger encode_integer, WriteValue write_value)
{
constexpr std::size_t max_run = max_reserve_size / integer_encoding_space;
auto it = j.m_data.m_value.array->begin();
const auto end = j.m_data.m_value.array->end();
while (it != end)
{
if (it->is_number_integer())
{
CharType* const start = oa.reserve(max_run * integer_encoding_space);
CharType* out = start;
for (std::size_t n = 0; n < max_run && it != end && it->is_number_integer(); ++n, ++it)
{
out += encode_integer(out, *it);
}
oa.commit(static_cast<std::size_t>(out - start));
}
else
{
write_value(*it);
++it;
}
}
}
/*!
@brief write a number_integer or number_unsigned value as a CBOR integer
A negative integer n has major type 1 and the argument -1 - n, which is ~n.
Kept out of line, like the other per-value paths, so that write_cbor stays
small (runs of integers in an array are encoded inline instead).
*/
JSON_HEDLEY_NEVER_INLINE
void write_cbor_integer(const BasicJsonType& j)
{
oa.commit(encode_cbor_integer(oa.reserve(integer_encoding_space), j));
}
/// encode a number_integer or number_unsigned value to @a out (see
/// @ref write_cbor_integer) and return the number of characters
std::size_t encode_cbor_integer(CharType* out, const BasicJsonType& j) const noexcept
{
std::uint64_t sign_mask = 0;
const std::uint64_t raw = integer_bits(j, sign_mask);
// ~raw is -1 - n in the 64 bits that are written
return encode_cbor_integer_head(out, static_cast<std::uint8_t>(sign_mask & 0x20u), raw ^ sign_mask);
}
/*!
@brief whether the MessagePack integers of BasicJsonType are written by
@ref encode_msgpack_integer
It picks the format from the low 64 bits; a wider number type keeps the
range checks of @ref write_msgpack on the whole value.
*/
static constexpr bool msgpack_integers_fit_64_bits() noexcept
{
return sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t)
&& sizeof(typename BasicJsonType::number_unsigned_t) <= sizeof(std::uint64_t);
}
/*!
@brief write a number_integer or number_unsigned value as a MessagePack
integer without branching on its sign or size
Same output as @ref write_msgpack: a non-negative integer as positive fixint
(< 128) or uint 8/16/32/64, a negative one as negative fixint (>= -32) or
int 8/16/32/64. Kept out of line (see @ref write_cbor_integer).
*/
JSON_HEDLEY_NEVER_INLINE
void write_msgpack_integer(const BasicJsonType& j, std::true_type /*fits_64_bits*/)
{
oa.commit(encode_msgpack_integer(oa.reserve(integer_encoding_space), j));
}
/// encode a number_integer or number_unsigned value to @a out (see
/// @ref write_msgpack_integer) and return the number of characters
std::size_t encode_msgpack_integer(CharType* out, const BasicJsonType& j) const noexcept
{
std::uint64_t sign_mask = 0;
const std::uint64_t raw = integer_bits(j, sign_mask);
// A negative n fits int 8/16/32 exactly if 2 * (-1 - n) + 1 fits
// uint 8/16/32, so x gives the size class of either sign.
const std::uint64_t is_negative = sign_mask & 1u;
const std::uint64_t x = ((raw ^ sign_mask) << is_negative) | is_negative;
// A fixint is at most 127, or at least -32 (x at most 63). It is in
// size class 1 and is moved to class 0, the first byte, by subtracting 1.
const auto is_fixint = static_cast<unsigned>(x <= (127u ^ (sign_mask & 64u)));
const unsigned size_class = integer_size_class(x) - is_fixint;
// the first byte: the low byte of the integer for a fixint, else the
// marker 0xCC..0xCF (uint) or 0xD0..0xD3 (int), selected through a mask
const std::uint64_t fixint_mask = std::uint64_t{0} - is_fixint;
const std::uint64_t marker = 0xCBu + size_class + (sign_mask & 4u);
const auto first_byte = static_cast<std::uint8_t>((raw & fixint_mask) | (marker & ~fixint_mask));
return encode_first_byte_and_tail(out, first_byte, size_class, raw);
}
/// write a number_integer or number_unsigned value of a number type wider
/// than 64 bits as a MessagePack integer, with range checks on the whole value
void write_msgpack_integer(const BasicJsonType& j, std::false_type /*fits_64_bits*/)
{
if (j.m_data.m_type == value_t::number_unsigned)
{
write_msgpack_unsigned(static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned));
return;
}
if (j.m_data.m_value.number_integer >= 0)
{
// MessagePack does not differentiate between positive
// signed integers and unsigned integers.
write_msgpack_unsigned(static_cast<std::uint64_t>(j.m_data.m_value.number_integer));
}
else
{
if (j.m_data.m_value.number_integer >= -32)
{
// negative fixnum
write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer));
}
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int8_t>::min)() &&
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int8_t>::max)())
{
// int 8
oa.write_character(to_char_type(0xD0));
write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer));
}
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int16_t>::min)() &&
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int16_t>::max)())
{
// int 16
oa.write_character(to_char_type(0xD1));
write_number(static_cast<std::int16_t>(j.m_data.m_value.number_integer));
}
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int32_t>::min)() &&
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int32_t>::max)())
{
// int 32
oa.write_character(to_char_type(0xD2));
write_number(static_cast<std::int32_t>(j.m_data.m_value.number_integer));
}
else
{
// int 64
oa.write_character(to_char_type(0xD3));
write_number(static_cast<std::int64_t>(j.m_data.m_value.number_integer));
}
}
}
/*!
@brief write an array value (head and elements) as MessagePack
See @ref write_cbor_array.
*/
JSON_HEDLEY_NEVER_INLINE
void write_msgpack_array(const BasicJsonType& j, const std::size_t depth)
{
write_msgpack_array_prefix(j.m_data.m_value.array->size(), j);
write_msgpack_array_elements(j, depth, std::integral_constant<bool, msgpack_integers_fit_64_bits()>());
}
void write_msgpack_array_elements(const BasicJsonType& j, const std::size_t depth, std::true_type /*fits_64_bits*/)
{
write_array_elements(j, [this](CharType * out, const BasicJsonType & el)
{
return encode_msgpack_integer(out, el);
}, [this, depth](const BasicJsonType & el)
{
write_msgpack(el, depth + 1);
});
}
void write_msgpack_array_elements(const BasicJsonType& j, const std::size_t depth, std::false_type /*fits_64_bits*/)
{
for (const auto& el : *j.m_data.m_value.array)
{
write_msgpack(el, depth + 1);
}
}
/*! /*!
@brief write an object key as a CBOR text string @brief write an object key as a CBOR text string
@@ -8,7 +8,10 @@
#pragma once #pragma once
#include <algorithm> // max
#include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstring> // memcpy
#include <memory> // shared_ptr, make_shared #include <memory> // shared_ptr, make_shared
#include <string> // basic_string #include <string> // basic_string
#include <type_traits> // conditional, integral_constant, is_same #include <type_traits> // conditional, integral_constant, is_same
@@ -48,32 +51,170 @@ template<typename CharType> struct output_adapter_protocol
template<typename CharType> template<typename CharType>
using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>; using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
/// the largest number of characters a writer may reserve() from a sink at once;
/// output_vector_sink also appends writes of at least this size with insert()
constexpr std::size_t max_reserve_size = 256;
/// @brief non-virtual output sink writing into a std::vector /// @brief non-virtual output sink writing into a std::vector
/// ///
/// This sink is not part of the virtual output_adapter_protocol hierarchy: it is /// This sink is not part of the virtual output_adapter_protocol hierarchy: it is
/// passed to binary_writer by value as a template parameter, so /// passed to binary_writer by value as a template parameter, so
/// write_character()/write_characters() are ordinary (inlinable) calls with no /// write_character()/write_characters() are ordinary (inlinable) calls with no
/// vtable lookup and no shared_ptr. It is used for the common /// vtable lookup and no shared_ptr. It is used for the common
/// `to_cbor`/`to_msgpack`/... into a std::vector. output_vector_adapter below /// `to_cbor`/`to_msgpack`/... into a std::vector.
/// wraps this same sink to provide the virtual interface. ///
/// Most writes are a few bytes - a type marker, or a number of 1, 2, 4, or 8
/// bytes - and appending those with push_back() or insert() cost more than
/// encoding them: insert() is an out-of-line call into a routine that can insert
/// anywhere, and resize() is one as well with libstdc++. The sink therefore keeps
/// its own write position: it grows the vector geometrically ahead of the
/// position and copies each write to it, so a write is a capacity check and a
/// short copy. The destructor shrinks the vector to the bytes actually written,
/// also when a write throws; shrinking a vector never allocates. The sink owns
/// that trim, so it can be moved but not copied, and the vector must not be used
/// while the sink is alive.
template<typename CharType, typename AllocatorType = std::allocator<CharType>> template<typename CharType, typename AllocatorType = std::allocator<CharType>>
class output_vector_sink class output_vector_sink
{ {
public: public:
explicit output_vector_sink(std::vector<CharType, AllocatorType>& vec) noexcept explicit output_vector_sink(std::vector<CharType, AllocatorType>& vec) noexcept
: v(&vec), position(vec.size())
{}
output_vector_sink(const output_vector_sink&) = delete;
output_vector_sink& operator=(const output_vector_sink&) = delete;
output_vector_sink& operator=(output_vector_sink&&) = delete;
output_vector_sink(output_vector_sink&& other) noexcept
: v(other.v), position(other.position)
{
other.v = nullptr;
}
~output_vector_sink()
{
if (v != nullptr)
{
v->resize(position);
}
}
// always inlined: GCC otherwise keeps these out of line in a large
// translation unit, and a call per byte or number costs more than the write
JSON_HEDLEY_ALWAYS_INLINE
void write_character(CharType c)
{
if (JSON_HEDLEY_UNLIKELY(position == v->size()))
{
grow(1);
}
(*v)[position++] = c;
}
// no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null
// pointer with length 0 for empty strings/binary values; memcpy must not
// see it.
JSON_HEDLEY_ALWAYS_INLINE
void write_characters(const CharType* s, std::size_t length)
{
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
{
if (length >= max_reserve_size)
{
append(s, length);
return;
}
grow(length);
}
if (length != 0)
{
std::memcpy(v->data() + position, s, length * sizeof(CharType));
position += length;
}
}
/// @brief space for writing up to @a length characters directly
///
/// Returns a pointer to at least @a length (at most @ref max_reserve_size)
/// writable characters at the write position; @ref commit then keeps the
/// first ones of them. A writer can store whole words there and keep only
/// the bytes it needs, and keep its write pointer in a register across many
/// values instead of updating the sink after each.
JSON_HEDLEY_ALWAYS_INLINE
CharType* reserve(const std::size_t length)
{
JSON_ASSERT(length <= max_reserve_size);
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
{
grow(length);
}
return v->data() + position;
}
/// keep the first @a length characters written to the space from reserve()
JSON_HEDLEY_ALWAYS_INLINE
void commit(const std::size_t length) noexcept
{
position += length;
}
private:
/// make room for at least @a length more characters after the position;
/// kept out of line so that the write functions stay small enough for
/// GCC to inline them into the writer
JSON_HEDLEY_NEVER_INLINE
void grow(const std::size_t length)
{
// use the capacity the vector already has, else at least double it;
// never less than 64 characters, so that a small value does not
// reallocate for every few bytes
const std::size_t needed = position + length;
v->resize((std::max)(std::size_t{64}, needed <= v->capacity()
? v->capacity()
: (std::max)(needed, v->capacity() * 2)));
}
/// append @a length (at least max_reserve_size) characters that do not fit
/// the space ahead of the position: insert() copies them once into the
/// grown vector, where grow() would first zero the new space and then copy
/// over it - noticeable for a large string or binary value
JSON_HEDLEY_NEVER_INLINE
void append(const CharType* s, const std::size_t length)
{
v->resize(position);
if (v->capacity() - position < length)
{
// grow geometrically, with room for the writes that follow, so
// that the next grow() need not reallocate right away
v->reserve((std::max)(v->capacity() * 2, position + length + max_reserve_size));
}
v->insert(v->end(), s, s + length);
position = v->size();
}
std::vector<CharType, AllocatorType>* v;
/// number of characters of *v written so far
std::size_t position;
};
/// output adapter for byte vectors
template<typename CharType, typename AllocatorType = std::allocator<CharType>>
class output_vector_adapter : public output_adapter_protocol<CharType>
{
public:
explicit output_vector_adapter(std::vector<CharType, AllocatorType>& vec) noexcept
: v(vec) : v(vec)
{} {}
void write_character(CharType c) void write_character(CharType c) override
{ {
v.push_back(c); v.push_back(c);
} }
// no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null // no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null
// pointer with length 0 for empty strings/binary values. Appending an empty // pointer with length 0 for empty strings/binary values; appending an empty
// range is a no-op; the type-erased path tolerates this via the (unattributed) // range is a no-op
// virtual base, and the concrete sink must do the same. void write_characters(const CharType* s, std::size_t length) override
void write_characters(const CharType* s, std::size_t length)
{ {
v.insert(v.end(), s, s + length); v.insert(v.end(), s, s + length);
} }
@@ -82,33 +223,6 @@ class output_vector_sink
std::vector<CharType, AllocatorType>& v; std::vector<CharType, AllocatorType>& v;
}; };
/// output adapter for byte vectors
///
/// The appending itself lives in output_vector_sink; this class only adds the
/// virtual output_adapter_protocol interface on top of it, so both the
/// type-erased and the templated path share one implementation.
template<typename CharType, typename AllocatorType = std::allocator<CharType>>
class output_vector_adapter : public output_adapter_protocol<CharType>
{
public:
explicit output_vector_adapter(std::vector<CharType, AllocatorType>& vec) noexcept
: sink(vec)
{}
void write_character(CharType c) override
{
sink.write_character(c);
}
void write_characters(const CharType* s, std::size_t length) override
{
sink.write_characters(s, length);
}
private:
output_vector_sink<CharType, AllocatorType> sink;
};
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
/// output adapter for output streams /// output adapter for output streams
template<typename CharType> template<typename CharType>
@@ -188,8 +302,24 @@ class output_adapter_sink
oa->write_characters(s, length); oa->write_characters(s, length);
} }
/// space for writing up to @a length characters (see
/// output_vector_sink::reserve)
CharType* reserve(const std::size_t length) noexcept
{
JSON_ASSERT(length <= max_reserve_size);
static_cast<void>(length);
return reserved.data();
}
/// write the first @a length characters written to the space from reserve()
void commit(const std::size_t length)
{
oa->write_characters(reserved.data(), length);
}
private: private:
output_adapter_t<CharType> oa; output_adapter_t<CharType> oa;
std::array<CharType, max_reserve_size> reserved{};
}; };
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits /// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
+181 -6
View File
@@ -42,9 +42,14 @@
// each of these, not only the forward declarations from json_fwd.hpp, so IWYU's suggestion to // each of these, not only the forward declarations from json_fwd.hpp, so IWYU's suggestion to
// drop them (nothing in this file otherwise names the type) would break every downstream // drop them (nothing in this file otherwise names the type) would break every downstream
// translation unit that relies on basic_json<>'s defaults actually being usable. // translation unit that relies on basic_json<>'s defaults actually being usable.
#include <nlohmann/adl_serializer.hpp> // IWYU pragma: keep // export: client code names what these headers declare (nlohmann::json and ordered_json from
// json_fwd.hpp, adl_serializer for specializations, json_ref behind brace initialization, the
// NLOHMANN_DEFINE_* and NLOHMANN_JSON_NAMESPACE_* macros, and the literals at the end of this file), and include analyzers must
// attribute it to this header, not tell clients to include the internal ones (issue #5812).
// tests/iwyu_consumer checks this.
#include <nlohmann/adl_serializer.hpp> // IWYU pragma: export
#include <nlohmann/byte_container_with_subtype.hpp> #include <nlohmann/byte_container_with_subtype.hpp>
#include <nlohmann/detail/abi_macros.hpp> #include <nlohmann/detail/abi_macros.hpp> // IWYU pragma: export
#include <nlohmann/detail/conversions/from_json.hpp> // IWYU pragma: keep #include <nlohmann/detail/conversions/from_json.hpp> // IWYU pragma: keep
#include <nlohmann/detail/conversions/to_json.hpp> // IWYU pragma: keep #include <nlohmann/detail/conversions/to_json.hpp> // IWYU pragma: keep
#include <nlohmann/detail/exceptions.hpp> #include <nlohmann/detail/exceptions.hpp>
@@ -61,8 +66,8 @@
#include <nlohmann/detail/iterators/primitive_iterator.hpp> #include <nlohmann/detail/iterators/primitive_iterator.hpp>
#include <nlohmann/detail/json_custom_base_class.hpp> #include <nlohmann/detail/json_custom_base_class.hpp>
#include <nlohmann/detail/json_pointer.hpp> #include <nlohmann/detail/json_pointer.hpp>
#include <nlohmann/detail/json_ref.hpp> #include <nlohmann/detail/json_ref.hpp> // IWYU pragma: export
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp> // IWYU pragma: export
#include <nlohmann/detail/meta/detected.hpp> #include <nlohmann/detail/meta/detected.hpp>
#include <nlohmann/detail/string_concat.hpp> #include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp> #include <nlohmann/detail/string_escape.hpp>
@@ -74,7 +79,7 @@
#include <nlohmann/detail/output/serializer.hpp> #include <nlohmann/detail/output/serializer.hpp>
#include <nlohmann/detail/recursion_depth_limit.hpp> #include <nlohmann/detail/recursion_depth_limit.hpp>
#include <nlohmann/detail/value_t.hpp> #include <nlohmann/detail/value_t.hpp>
#include <nlohmann/json_fwd.hpp> #include <nlohmann/json_fwd.hpp> // IWYU pragma: export
#include <nlohmann/ordered_map.hpp> // IWYU pragma: keep #include <nlohmann/ordered_map.hpp> // IWYU pragma: keep
#include <nlohmann/thirdparty/hedley/hedley.hpp> #include <nlohmann/thirdparty/hedley/hedley.hpp>
@@ -7449,6 +7454,176 @@ public:
} }
public: public:
/// @brief creates a diff as a JSON Merge Patch
/// @sa https://json.nlohmann.me/api/basic_json/merge_diff/
JSON_HEDLEY_WARN_UNUSED_RESULT
static basic_json merge_diff(const basic_json& source, const basic_json& target)
{
// unless both are objects, the patch is the target itself
if (!source.is_object() || !target.is_object())
{
return target;
}
return merge_diff_recursively(source, target, 0);
}
private:
/// @brief two objects @ref merge_diff_iteratively is diffing
struct merge_diff_frame
{
merge_diff_frame(const basic_json* source_, const basic_json* target_)
: source(source_), target(target_), member(source_->cbegin()), patch(value_t::object)
{}
// declared for GCC's -Weffc++, as in diff_frame
merge_diff_frame(const merge_diff_frame&) = default;
merge_diff_frame(merge_diff_frame&&) = default;
merge_diff_frame& operator=(const merge_diff_frame&) = default;
merge_diff_frame& operator=(merge_diff_frame&&) = default;
~merge_diff_frame() = default;
/// the objects being diffed
const basic_json* source;
const basic_json* target;
/// the member of source being diffed; advanced once it is done
const_iterator member;
/// the patch for the members of source diffed so far
basic_json patch;
};
/// @brief add the members of object @a target that @a source does not
/// have to the merge patch @a result
static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target)
{
for (auto it = target.begin(); it != target.end(); ++it)
{
auto itf = source.find(it.key());
if (itf == source.end())
{
result[it.key()] = it.value();
}
}
}
/*!
@brief @ref merge_diff of two objects at nesting level @a depth
Diffing two nested objects calls this function again, once per nesting
level, so values nested deeply enough would exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
detail::recursion_depth_limit levels have been entered, @ref
merge_diff_iteratively diffs what is left without the call stack.
*/
static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth)
{
JSON_ASSERT(source.is_object() && target.is_object());
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return merge_diff_iteratively(source, target);
}
basic_json result(value_t::object);
for (auto it = source.begin(); it != source.end(); ++it)
{
auto itf = target.find(it.key());
if (itf != target.end())
{
if (it.value().is_object() && itf.value().is_object())
{
auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1);
if (!diff.empty())
{
result[it.key()] = std::move(diff);
}
}
else if (it.value() != itf.value())
{
result[it.key()] = itf.value();
}
}
else
{
result[it.key()] = value_t::null;
}
}
merge_diff_added_members(result, source, target);
return result;
}
/*!
@brief @ref merge_diff of two objects without the call stack
Produces the same patch as @ref merge_diff_recursively, keeping the objects
being diffed on an explicit stack instead of descending into them. Two
nested objects are still diffed completely before the next member, so the
patch has its members in the same order as in the recursive version. Only
reached for objects nested more deeply than @ref
detail::recursion_depth_limit.
*/
static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target)
{
// the patch of source and target, set once their frame is popped
basic_json result;
std::vector<merge_diff_frame> stack;
stack.emplace_back(&source, &target);
while (!stack.empty())
{
// the frame's members are copied out and it is changed through
// stack.back(): pushing a frame for nested objects or popping
// this one would invalidate a reference to it
const basic_json* const s = stack.back().source;
const basic_json* const t = stack.back().target;
const const_iterator it = stack.back().member;
if (it != s->cend())
{
const const_iterator itf = t->find(it.key());
if (itf != t->cend())
{
if (it.value().is_object() && itf.value().is_object())
{
// member is advanced once their patch is under its key
stack.emplace_back(&it.value(), &itf.value());
continue;
}
if (it.value() != itf.value())
{
stack.back().patch[it.key()] = itf.value();
}
}
else
{
stack.back().patch[it.key()] = value_t::null;
}
++stack.back().member;
continue;
}
merge_diff_added_members(stack.back().patch, *s, *t);
// these objects are done: continue with the ones they are in
basic_json patch = std::move(stack.back().patch);
stack.pop_back();
if (stack.empty())
{
result = std::move(patch);
}
else
{
if (!patch.empty())
{
stack.back().patch[stack.back().member.key()] = std::move(patch);
}
++stack.back().member;
}
}
return result;
}
/// @} /// @}
}; };
@@ -7625,7 +7800,7 @@ struct formatter<nlohmann::NLOHMANN_BASIC_JSON_TPL, char> // NOLINT(cert-dcl58-c
// where needed. // where needed.
#ifndef JSON_NO_AUTOMATIC_UDLS #ifndef JSON_NO_AUTOMATIC_UDLS
// NOLINTNEXTLINE(misc-header-include-cycle): json_literals.hpp includes this header // NOLINTNEXTLINE(misc-header-include-cycle): json_literals.hpp includes this header
#include <nlohmann/json_literals.hpp> #include <nlohmann/json_literals.hpp> // IWYU pragma: export
#endif #endif
#endif // INCLUDE_NLOHMANN_JSON_HPP_ #endif // INCLUDE_NLOHMANN_JSON_HPP_
+1081 -198
View File
File diff suppressed because it is too large. Load diff
+62
View File
@@ -0,0 +1,62 @@
// __ _____ _____ _____
// __| | __| | | | 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
// See values.cpp: the conversion macros and nlohmann::adl_serializer must be available through
// <nlohmann/json.hpp> alone. NLOHMANN_JSON_SERIALIZE_ENUM (from the same header as
// NLOHMANN_DEFINE_TYPE_INTRUSIVE) is left out: IWYU attributes the basic_json comparison in its
// expansion to an unrelated standard header, the ambiguity described for json.hpp in cmake/ci.cmake.
#include <string>
#include <nlohmann/json.hpp>
namespace ns
{
struct person
{
std::string name;
int age;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(person, name, age)
};
struct legacy
{
int value;
};
} // namespace ns
namespace nlohmann
{
template<>
struct adl_serializer<ns::legacy>
{
static void to_json(json& j, const ns::legacy& l)
{
j = l.value;
}
static void from_json(const json& j, ns::legacy& l)
{
l.value = j.get<int>();
}
};
} // namespace nlohmann
int main()
{
ns::person ada;
ada.name = "Ada";
ada.age = 36;
ns::legacy one;
one.value = 1;
const nlohmann::json j = ada;
const auto p = j.get<ns::person>();
const auto l = nlohmann::json(one).get<ns::legacy>();
return p.age - 36 + l.value - 1;
}
+19
View File
@@ -0,0 +1,19 @@
// __ _____ _____ _____
// __| | __| | | | 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
// See values.cpp: the user-defined literals must be available through <nlohmann/json.hpp> alone.
#include <nlohmann/json.hpp>
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
int main()
{
const auto j = R"({"a": [1, 2]})"_json;
return j.at("/a/1"_json_pointer).get<int>() - 2;
}
+30
View File
@@ -0,0 +1,30 @@
// __ _____ _____ _____
// __| | __| | | | 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
// Uses the library the way a client does, including only <nlohmann/json.hpp>. ci_single_binaries
// runs include-what-you-use and clang-tidy's misc-include-cleaner on this file and fails if either
// asks for another nlohmann header, which would mean json.hpp does not export it (issue #5812).
#include <cstdint>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
int main()
{
// brace initialization (nlohmann::json, detail::json_ref)
nlohmann::json j = {{"pi", 3.141}, {"list", {1, 0, 2}}};
const nlohmann::ordered_json o = {{"b", 1}, {"a", 2}};
j["parsed"] = nlohmann::json::parse(R"({"happy": true})");
j["binary"] = nlohmann::json::binary(std::vector<std::uint8_t> {0xCA, 0xFE}, 42);
const std::string s = j.dump() + o.dump();
return s.empty() ? 1 : 0;
}
+264 -1
View File
@@ -13,6 +13,7 @@ using nlohmann::json;
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -35,6 +36,17 @@ std::vector<json> test_values()
big_object[std::to_string(i)] = i; big_object[std::to_string(i)] = i;
} }
// large strings and binary values between small ones: a write too large
// for the space the vector sink has ahead takes a different path than a
// small one, and the small writes after it must still land behind it
json large_payloads = json::array();
for (int i = 0; i < 50; ++i)
{
large_payloads.push_back(std::string(300, static_cast<char>('a' + (i % 26))));
large_payloads.push_back(i);
large_payloads.push_back(json::binary(std::vector<std::uint8_t>(static_cast<std::size_t>(100 * i), static_cast<std::uint8_t>(i))));
}
return return
{ {
json(nullptr), json(true), json(false), json(nullptr), json(true), json(false),
@@ -46,7 +58,8 @@ std::vector<json> test_values()
json::array(), json::object(), json::array(), json::object(),
json::array({1, 2, 3}), json({{"a", 1}, {"b", nullptr}}), json::array({1, 2, 3}), json({{"a", 1}, {"b", nullptr}}),
json({{"nested", {{"deep", json::array({1, "two", 3.0, nullptr})}}}}), json({{"nested", {{"deep", json::array({1, "two", 3.0, nullptr})}}}}),
big_array, big_object big_array, big_object, large_payloads,
json::binary(std::vector<std::uint8_t>(5000, 0x42))
}; };
} }
@@ -170,6 +183,256 @@ TEST_CASE("binary writer output sinks")
} }
} }
namespace
{
// the CBOR head of an integer, encoded independently of binary_writer
void reference_cbor_head(std::vector<std::uint8_t>& out, const std::uint8_t major_type, const std::uint64_t argument)
{
if (argument <= 23)
{
out.push_back(static_cast<std::uint8_t>(major_type + argument));
return;
}
int length = 8;
std::uint8_t additional = 27;
if (argument <= 0xFF)
{
length = 1;
additional = 24;
}
else if (argument <= 0xFFFF)
{
length = 2;
additional = 25;
}
else if (argument <= 0xFFFFFFFF)
{
length = 4;
additional = 26;
}
out.push_back(static_cast<std::uint8_t>(major_type + additional));
for (int i = length - 1; i >= 0; --i)
{
out.push_back(static_cast<std::uint8_t>(argument >> (8 * i)));
}
}
// the CBOR encoding of an integer value
void reference_cbor_integer(std::vector<std::uint8_t>& out, const json& j)
{
if (j.is_number_unsigned())
{
reference_cbor_head(out, 0x00, j.get<std::uint64_t>());
return;
}
const auto n = j.get<std::int64_t>();
if (n >= 0)
{
reference_cbor_head(out, 0x00, static_cast<std::uint64_t>(n));
}
else
{
reference_cbor_head(out, 0x20, static_cast<std::uint64_t>(-1 - n));
}
}
// the MessagePack encoding of an integer value, encoded independently of
// binary_writer
void reference_msgpack_integer(std::vector<std::uint8_t>& out, const json& j)
{
const auto put = [&out](const std::uint8_t marker, const std::uint64_t bits, const int length)
{
out.push_back(marker);
for (int i = length - 1; i >= 0; --i)
{
out.push_back(static_cast<std::uint8_t>(bits >> (8 * i)));
}
};
if (j.is_number_unsigned() || j.get<std::int64_t>() >= 0)
{
const auto u = j.get<std::uint64_t>();
if (u < 128)
{
out.push_back(static_cast<std::uint8_t>(u));
}
else if (u <= 0xFF)
{
put(0xCC, u, 1);
}
else if (u <= 0xFFFF)
{
put(0xCD, u, 2);
}
else if (u <= 0xFFFFFFFF)
{
put(0xCE, u, 4);
}
else
{
put(0xCF, u, 8);
}
return;
}
const auto n = j.get<std::int64_t>();
const auto bits = static_cast<std::uint64_t>(n);
if (n >= -32)
{
out.push_back(static_cast<std::uint8_t>(bits));
}
else if (n >= -128)
{
put(0xD0, bits, 1);
}
else if (n >= -32768)
{
put(0xD1, bits, 2);
}
else if (n >= -2147483648LL)
{
put(0xD2, bits, 4);
}
else
{
put(0xD3, bits, 8);
}
}
// integers at every boundary of the CBOR and MessagePack sizes, signed and
// unsigned
std::vector<json> boundary_integers()
{
std::vector<json> result;
const std::uint64_t boundaries[] =
{
0, 1, 22, 23, 24, 25, 31, 32, 33, 126, 127, 128, 129, 254, 255, 256, 257,
32766, 32767, 32768, 32769, 65534, 65535, 65536, 65537,
2147483646ULL, 2147483647ULL, 2147483648ULL, 2147483649ULL,
4294967294ULL, 4294967295ULL, 4294967296ULL, 4294967297ULL,
9223372036854775806ULL, 9223372036854775807ULL, 9223372036854775808ULL,
18446744073709551614ULL, 18446744073709551615ULL
};
for (const std::uint64_t u : boundaries)
{
result.emplace_back(u);
if (u <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
{
const auto n = static_cast<std::int64_t>(u);
result.emplace_back(static_cast<json::number_integer_t>(n));
result.emplace_back(static_cast<json::number_integer_t>(-n));
result.emplace_back(static_cast<json::number_integer_t>(-n - 1));
}
}
return result;
}
} // namespace
// Arrays of integers are written in runs straight into space reserved from the
// sink, with branch-free encodings; check them against independent encoders for
// runs shorter and longer than one reservation, broken up by other values, and
// through each sink.
TEST_CASE("CBOR and MessagePack integer arrays")
{
const std::vector<json> integers = boundary_integers();
for (const std::size_t length :
{
std::size_t{1}, std::size_t{27}, std::size_t{28}, std::size_t{29}, std::size_t{57}, std::size_t{1000}
})
{
for (const std::size_t break_every :
{
std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{28}
})
{
CAPTURE(length)
CAPTURE(break_every)
json array = json::array();
std::vector<std::uint8_t> expected_cbor;
std::vector<std::uint8_t> expected_msgpack;
for (std::size_t i = 0; i < length; ++i)
{
if (break_every != 0 && i % (break_every + 1) == break_every)
{
array.push_back("x");
expected_cbor.push_back(0x61);
expected_cbor.push_back('x');
expected_msgpack.push_back(0xA1);
expected_msgpack.push_back('x');
}
else
{
const json& value = integers[(i * 7) % integers.size()];
array.push_back(value);
reference_cbor_integer(expected_cbor, value);
reference_msgpack_integer(expected_msgpack, value);
}
}
std::vector<std::uint8_t> head;
reference_cbor_head(head, 0x80, length);
expected_cbor.insert(expected_cbor.begin(), head.begin(), head.end());
head.clear();
if (length <= 15)
{
head.push_back(static_cast<std::uint8_t>(0x90 + length));
}
else
{
head = {0xDC, static_cast<std::uint8_t>(length >> 8), static_cast<std::uint8_t>(length)};
}
expected_msgpack.insert(expected_msgpack.begin(), head.begin(), head.end());
CHECK(json::to_cbor(array) == expected_cbor);
CHECK(json::to_msgpack(array) == expected_msgpack);
std::vector<std::uint8_t> through_adapter = {0xAA};
json::to_cbor(array, through_adapter);
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected_cbor);
through_adapter = {0xAA};
json::to_msgpack(array, through_adapter);
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected_msgpack);
std::ostringstream stream;
json::to_cbor(array, stream);
std::string streamed = stream.str();
CHECK(std::vector<std::uint8_t>(streamed.begin(), streamed.end()) == expected_cbor);
stream.str("");
json::to_msgpack(array, stream);
streamed = stream.str();
CHECK(std::vector<std::uint8_t>(streamed.begin(), streamed.end()) == expected_msgpack);
CHECK(json::from_cbor(expected_cbor) == array);
CHECK(json::from_msgpack(expected_msgpack) == array);
// the same array as object members
const json object = {{"a", array}, {"b", array}};
CHECK(json::from_cbor(json::to_cbor(object)) == object);
CHECK(json::from_msgpack(json::to_msgpack(object)) == object);
}
}
// single values, outside an array
for (const auto& value : integers)
{
CAPTURE(value)
std::vector<std::uint8_t> expected;
reference_cbor_integer(expected, value);
CHECK(json::to_cbor(value) == expected);
expected.clear();
reference_msgpack_integer(expected, value);
CHECK(json::to_msgpack(value) == expected);
}
}
// binary_reserve_hint() is documented as a *lower* bound on the serialized size, // binary_reserve_hint() is documented as a *lower* bound on the serialized size,
// so that reserving it up front can never leave the returned vector holding // so that reserving it up front can never leave the returned vector holding
// capacity beyond what the value actually needs. // capacity beyond what the value actually needs.
+4
View File
@@ -938,6 +938,10 @@ TEST_CASE("Eisel-Lemire float conversion")
const int k = i % 64; const int k = i % 64;
const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1)); const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1));
CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k); CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k);
// the mirror image: bit k set, arbitrary bits above it
const std::uint64_t y = (std::uint64_t{1} << k) | (k == 63 ? 0 : (a << (k + 1)));
CHECK(nlohmann::detail::count_trailing_zeros(y) == k);
} }
} }
+57
View File
@@ -8,6 +8,13 @@
#include "doctest_compatibility.h" #include "doctest_compatibility.h"
// capture whether JSON_DISABLE_ENUM_SERIALIZATION was enabled on the command
// line before including json.hpp, which #undefs it; std::byte is an enum, so
// converting std::vector<std::byte> needs enum serialization
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION
#endif
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <cstdint> #include <cstdint>
@@ -85,3 +92,53 @@ TEST_CASE("binary type whose value type is not std::uint8_t")
} }
#endif #endif
} }
#ifdef JSON_HAS_CPP_17
// moved from unit-regression3.cpp, so that file does not instantiate a second
// basic_json specialization (MinGW section limit, see windows.yml)
TEST_CASE("std::vector<std::byte> as binary type")
{
SECTION("issue #4804: from_cbor incompatible with std::vector<std::byte> as binary_t")
{
const std::vector<std::uint8_t> data = {0x80};
const auto decoded = byte_binary_json::from_cbor(data);
CHECK((decoded == byte_binary_json::array()));
}
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{
// Test that assigning a custom BinaryType directly creates a binary value, not an array
const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}};
const byte_binary_json j = original;
CHECK(j.is_binary());
CHECK(!j.is_array());
// Test round-tripping: extracting the binary value back as the custom container type
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted == original);
// Test that the default json alias behavior is unchanged: std::vector<uint8_t> -> array
const nlohmann::json default_json = std::vector<std::uint8_t> {1, 2, 3};
CHECK(default_json.is_array());
CHECK(!default_json.is_binary());
}
SECTION("discussion #4209 - custom BinaryType extraction from parsed array")
{
// Test that extracting a custom BinaryType from a parsed JSON array still works
// (not just from a binary-typed node)
const auto j = byte_binary_json::parse("[1,2,3]");
CHECK(j.is_array());
CHECK(!j.is_binary());
// Extracting as custom BinaryType should work from arrays
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted.size() == 3);
CHECK(extracted[0] == std::byte{1});
CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3});
}
#endif
}
#endif
+137
View File
@@ -35,6 +35,7 @@ using nlohmann::json;
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <valarray> #include <valarray>
#include <vector>
#include "sax_event_loggers.hpp" #include "sax_event_loggers.hpp"
@@ -1250,3 +1251,139 @@ TEST_CASE_TEMPLATE("deserialization of different character types (UTF-32)", T, c
CHECK(json::sax_parse(v, &l)); CHECK(json::sax_parse(v, &l));
CHECK(l.events.size() == 1); CHECK(l.events.size() == 1);
} }
namespace
{
// records the position and message of a parse error; reporting the error via
// SAX does not throw, so the test also runs with JSON_NOEXCEPTION
struct SaxErrorRecorder : public SaxEventLogger
{
bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& ex) override
{
error = std::to_string(position) + " " + ex.what();
return false;
}
std::string error {}; // NOLINT(readability-redundant-member-init)
};
} // namespace
TEST_CASE("parse error positions are the same for contiguous and stream input")
{
// contiguous input derives the position from the input on error, stream
// input counts it while reading (see lexer::lazy_position); the cases
// cover newlines before the error, ungetting a newline after a number,
// newlines inside a string, a comment, and reads at the end of input
struct test_case
{
std::string input;
std::size_t byte;
std::string message;
};
const std::vector<test_case> cases =
{
{"[\n 1,\n 2\n", 12, "[json.exception.parse_error.101] parse error at line 4, column 1: syntax error while parsing array - unexpected end of input; expected ']'"},
{"[1\n,2,\n]", 8, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal"},
{"[1\n x]", 5, "[json.exception.parse_error.101] parse error at line 2, column 2: syntax error while parsing array - invalid literal; last read: '1<U+000A> x'; expected ']'"},
{"1\n2", 3, "[json.exception.parse_error.101] parse error at line 2, column 1: syntax error while parsing value - unexpected number literal; expected end of input"},
{
"{\"a\":\n\"b\nc\"}", 9, R"([json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid string: control character U+000A (LF) must be escaped to \u000A or \n; last read: '"b<U+000A>')"
},
{"/* a\n b */\n x", 13, "[json.exception.parse_error.101] parse error at line 3, column 2: syntax error while parsing value - invalid literal; last read: '/* a<U+000A> b */<U+000A> x'"},
{"\n\n", 3, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal"},
{"[\"a\",\n 1e\n]", 10, "[json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid number; expected '+', '-', or digit after exponent; last read: '1e<U+000A>'"},
{"[tru\n]", 5, "[json.exception.parse_error.101] parse error at line 2, column 0: syntax error while parsing value - invalid literal; last read: '[tru<U+000A>'"},
};
for (const auto& c : cases)
{
CAPTURE(c.input)
const std::string expected = std::to_string(c.byte) + " " + c.message;
SaxErrorRecorder string_sax;
CHECK_FALSE(json::sax_parse(c.input, &string_sax, json::input_format_t::json, true, true));
CHECK(string_sax.error == expected);
SaxErrorRecorder pointer_sax;
CHECK_FALSE(json::sax_parse(c.input.data(), c.input.data() + c.input.size(), &pointer_sax, json::input_format_t::json, true, true));
CHECK(pointer_sax.error == expected);
std::istringstream is(c.input);
SaxErrorRecorder stream_sax;
CHECK_FALSE(json::sax_parse(is, &stream_sax, json::input_format_t::json, true, true));
CHECK(stream_sax.error == expected);
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("indentation runs of every length around the 8-byte skip")
{
// contiguous input skips the spaces after a newline 8 bytes at a time (see
// lexer::skip_indentation), stream input reads them one by one; results,
// byte offsets, and messages must be the same for runs that end anywhere
// relative to a word boundary, including at the end of the input
const auto parse_result = [](const std::string & input, bool contiguous)
{
try
{
if (contiguous)
{
return json::parse(input.data(), input.data() + input.size(), nullptr, true, true).dump();
}
std::istringstream is(input);
return json::parse(is, nullptr, true, true).dump();
}
catch (const json::parse_error& e)
{
return std::to_string(e.byte).append(" ").append(e.what());
}
};
// replace every '~' in a pattern by an indentation run
const auto indented = [](const std::string & pattern, const std::string & indent)
{
std::string result;
for (const char c : pattern)
{
if (c == '~')
{
result += indent;
}
else
{
result += c;
}
}
return result;
};
const std::vector<std::string> patterns =
{
"[\n~1,\n~2\n]",
"[\n~1,\n~x]",
"[\n~1,\n~",
"1\n~",
"[\n~\t1,\n~\r\n~2]",
"[\n~/* c */ 1]",
"[\n~1\n~2]",
};
for (std::size_t n = 0; n <= 33; ++n)
{
const std::string indent(n, ' ');
for (const auto& pattern : patterns)
{
const std::string input = indented(pattern, indent);
CAPTURE(n)
CAPTURE(input)
const std::string expected = parse_result(input, false);
CHECK(parse_result(input, true) == expected);
CHECK(json::accept(input.data(), input.data() + input.size(), true) == json::accept(input, true));
}
}
// and against fixed expectations, in case both paths broke the same way
CHECK(parse_result(indented("[\n~x]", std::string(16, ' ')), true) == "19 [json.exception.parse_error.101] parse error at line 2, column 17: syntax error while parsing value - invalid literal; last read: '[<U+000A> x'");
CHECK(parse_result(indented("[\n~1]", std::string(17, ' ')), true) == "[1]");
}
#endif
+17 -2
View File
@@ -567,9 +567,9 @@ TEST_CASE("Regression tests for extended diagnostics")
} }
} }
TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()") TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch() and merge_diff()")
{ {
// Both merge objects nested more than detail::recursion_depth_limit() // All three handle objects nested more than detail::recursion_depth_limit()
// (128) levels deep without recursing; the values they add or replace // (128) levels deep without recursing; the values they add or replace
// there must still know their parents. // there must still know their parents.
// The values are built rather than parsed, so that the expected messages // The values are built rather than parsed, so that the expected messages
@@ -615,4 +615,19 @@ TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch
CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error);
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
} }
SECTION("merge_diff()")
{
// target and patch differ only in their innermost members, so the
// diff of the two is nested as deeply: {"a": ... {"x": null, "y": 2}}
const json diff = json::merge_diff(target, patch); // NOLINT(readability-suspicious-call-argument)
const json* p = &diff;
for (std::size_t i = 0; i < depth; ++i)
{
p = &p->at("a");
}
CHECK(p->at("x").is_null());
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
}
} }
File diff suppressed because it is too large. Load diff
-48
View File
@@ -58,11 +58,6 @@ using ordered_json = nlohmann::ordered_json;
#include <optional> #include <optional>
#elif __has_include(<experimental/optional>) #elif __has_include(<experimental/optional>)
#endif #endif
/////////////////////////////////////////////////////////////////////
// for #4804
/////////////////////////////////////////////////////////////////////
using json_4804 = nlohmann::json::with_binary_t<std::vector<std::byte>>;
#endif #endif
#ifdef JSON_HAS_CPP_20 #ifdef JSON_HAS_CPP_20
@@ -458,49 +453,6 @@ TEST_CASE("regression tests 3")
#endif #endif
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
SECTION("issue #4804: from_cbor incompatible with std::vector<std::byte> as binary_t")
{
const std::vector<std::uint8_t> data = {0x80};
const auto decoded = json_4804::from_cbor(data);
CHECK((decoded == json_4804::array()));
}
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{
// Test that assigning a custom BinaryType directly creates a binary value, not an array
const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}};
const json_4804 j = original;
CHECK(j.is_binary());
CHECK(!j.is_array());
// Test round-tripping: extracting the binary value back as the custom container type
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted == original);
// Test that the default json alias behavior is unchanged: std::vector<uint8_t> -> array
const json default_json = std::vector<std::uint8_t> {1, 2, 3};
CHECK(default_json.is_array());
CHECK(!default_json.is_binary());
}
SECTION("discussion #4209 - custom BinaryType extraction from parsed array")
{
// Test that extracting a custom BinaryType from a parsed JSON array still works
// (not just from a binary-typed node)
const auto j = json_4804::parse("[1,2,3]");
CHECK(j.is_array());
CHECK(!j.is_binary());
// Extracting as custom BinaryType should work from arrays
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted.size() == 3);
CHECK(extracted[0] == std::byte{1});
CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3});
}
#endif
SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit")
{ {
const json jval{}; const json jval{};