mirror of
https://github.com/nlohmann/json.git
synced 2026-09-25 09:20:32 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6659373722 | ||
|
|
01b53c8c15 | ||
|
|
cc472af13f | ||
|
|
80bf54a5a2 | ||
|
|
aada27405d | ||
|
|
3901b223e5 | ||
|
|
c5e380c617 | ||
|
|
169edf2272 | ||
|
|
ead6a66c97 | ||
|
|
af8083cd1f | ||
|
|
a645352bc6 | ||
|
|
e6a2c674ef |
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
- [ ] The changes are described in detail, both the what and why.
|
- [ ] The changes are described in detail, both the what and why.
|
||||||
- [ ] If applicable, an [existing issue](https://github.com/nlohmann/json/issues) is referenced.
|
- [ ] If applicable, an [existing issue](https://github.com/nlohmann/json/issues) is referenced.
|
||||||
|
- [ ] If applicable, a fixed [OSS-Fuzz](https://issues.oss-fuzz.com) issue is referenced as `OSS-Fuzz: <id>` (see [fuzz testing](https://github.com/nlohmann/json/blob/develop/tests/fuzzing.md#handling-oss-fuzz-reports)).
|
||||||
- [ ] The [Code coverage](https://coveralls.io/github/nlohmann/json) remained at 100%. A test case for every new line of code.
|
- [ ] The [Code coverage](https://coveralls.io/github/nlohmann/json) remained at 100%. A test case for every new line of code.
|
||||||
- [ ] If applicable, the [documentation](https://json.nlohmann.me) is updated.
|
- [ ] If applicable, the [documentation](https://json.nlohmann.me) is updated.
|
||||||
- [ ] The source code is amalgamated by running `make amalgamate`.
|
- [ ] The source code is amalgamated by running `make amalgamate`.
|
||||||
|
|||||||
+4
-4
@@ -300,10 +300,10 @@ add_custom_target(ci_test_skiplibraryversioncheck
|
|||||||
# Disable thread-local storage.
|
# Disable thread-local storage.
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Without thread-local storage, the copy constructor cannot bound its descent
|
# Without thread-local storage, copying and comparing cannot bound their
|
||||||
# and copies every object and array without the call stack. That path is
|
# descent and handle every object and array without the call stack. Those paths
|
||||||
# otherwise only reached by values nested deeper than the bound, so this target
|
# are otherwise only reached by values nested deeper than the bound, so this
|
||||||
# is what runs the whole test suite through it.
|
# target is what runs the whole test suite through them.
|
||||||
add_custom_target(ci_test_no_thread_local
|
add_custom_target(ci_test_no_thread_local
|
||||||
COMMAND ${CMAKE_COMMAND}
|
COMMAND ${CMAKE_COMMAND}
|
||||||
-DCMAKE_BUILD_TYPE=Debug -GNinja
|
-DCMAKE_BUILD_TYPE=Debug -GNinja
|
||||||
|
|||||||
@@ -7,16 +7,16 @@
|
|||||||
When defined, the library does not use `#!cpp thread_local` storage. This is relevant for the few environments whose
|
When defined, the library does not use `#!cpp thread_local` storage. This is relevant for the few environments whose
|
||||||
toolchain does not support it.
|
toolchain does not support it.
|
||||||
|
|
||||||
The copy constructor copies the first levels of a value by copying the containers, which copy their elements, and
|
Copying a value and comparing two values both descend into the first levels by letting the containers copy or compare
|
||||||
completes whatever is nested deeper than that without the call stack, so that copying a value cannot exhaust the stack
|
themselves, and finish whatever is nested deeper than that without the call stack, so that neither can exhaust the stack
|
||||||
however deeply it is nested. It counts the levels it has descended into in a `#!cpp thread_local` variable, as a counter
|
however deeply the values are nested. Each counts the levels it has descended into in a `#!cpp thread_local` variable, as
|
||||||
shared between threads would be raced.
|
a counter shared between threads would be raced.
|
||||||
|
|
||||||
Without that counter, no descent can be bounded safely, so objects and arrays are copied without the call stack right
|
Without those counters, no descent can be bounded safely, so objects and arrays are copied and compared without the call
|
||||||
away. Copying keeps working exactly as it does otherwise - the same values come out, and deeply nested values are copied
|
stack right away. Both keep working exactly as they do otherwise - the same values come out, the same comparisons hold,
|
||||||
just as safely - but copying is slower, because the containers no longer copy themselves. Copying the benchmark
|
and deeply nested values are handled just as safely - but both are slower, because the containers no longer copy or
|
||||||
documents takes 9% (`canada.json`) to 34% (`twitter.json`) longer; values built mostly from objects are affected the
|
compare themselves. Copying the benchmark documents takes 9% (`canada.json`) to 34% (`twitter.json`) longer, and
|
||||||
most.
|
comparing two equal ones 10% (`citm_catalog.json`) to 90% (`canada.json`) longer.
|
||||||
|
|
||||||
## Default definition
|
## Default definition
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ By default, `#!cpp JSON_NO_THREAD_LOCAL` is not defined.
|
|||||||
|
|
||||||
The library defines it by itself for Clang targeting MinGW, which does not survive the `#!cpp thread_local` storage:
|
The library defines it by itself for Clang targeting MinGW, which does not survive the `#!cpp thread_local` storage:
|
||||||
copying a value segfaults there, with both old and current Clang versions, while GCC targeting MinGW is unaffected.
|
copying a value segfaults there, with both old and current Clang versions, while GCC targeting MinGW is unaffected.
|
||||||
|
Copying and comparing fall back to working without the call stack there, as they do whenever the macro is defined.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Assurance case
|
||||||
|
|
||||||
|
This page argues why the library meets its security requirements. It describes the threats the library faces, where the
|
||||||
|
trust boundaries lie, and how the library's design and the [quality assurance](quality_assurance.md) counter these
|
||||||
|
threats. To report a vulnerability, see the [security policy](security_policy.md).
|
||||||
|
|
||||||
|
## Threat model
|
||||||
|
|
||||||
|
The library parses, stores, and serializes JSON values in memory. It does not open network connections, does not open
|
||||||
|
files (it only reads from streams or `std::FILE*` handles that the caller has already opened), does not read environment
|
||||||
|
variables, and does not implement cryptography or handle credentials.
|
||||||
|
|
||||||
|
The primary threat is therefore **untrusted input**: JSON text or binary data (BJData, BSON, CBOR, MessagePack, UBJSON)
|
||||||
|
that an attacker controls, passed to [`parse`](../api/basic_json/parse.md), [`accept`](../api/basic_json/accept.md),
|
||||||
|
[`sax_parse`](../api/basic_json/sax_parse.md), or one of the `from_*` functions such as
|
||||||
|
[`from_cbor`](../api/basic_json/from_cbor.md). Such input may try to
|
||||||
|
|
||||||
|
- make the library read or write out of bounds (malformed lengths, truncated input, invalid UTF-8),
|
||||||
|
- trigger undefined behavior (integer overflow in sizes or numbers, invalid casts),
|
||||||
|
- exhaust memory (huge announced sizes), or
|
||||||
|
- exhaust the call stack (deeply nested arrays and objects).
|
||||||
|
|
||||||
|
## Trust boundaries
|
||||||
|
|
||||||
|
- **Untrusted:** all serialized input read by the parser, the SAX interface, and the binary readers. The library must
|
||||||
|
handle every possible input by either producing a value or throwing a [`parse_error`](../home/exceptions.md#parse-errors)
|
||||||
|
(or returning `false` when exceptions are disabled for the call).
|
||||||
|
- **Trusted:** the C++ code that calls the library. Calling a function with violated preconditions, for instance
|
||||||
|
accessing an array with [`operator[]`](../api/basic_json/operator%5B%5D.md) out of range, is a programming error and
|
||||||
|
not a security boundary. Such preconditions are checked with [runtime assertions](../features/assertions.md) in debug
|
||||||
|
builds; functions such as [`at`](../api/basic_json/at.md) offer checked access with exceptions.
|
||||||
|
|
||||||
|
## Secure design
|
||||||
|
|
||||||
|
- **Strict parsing.** The parser accepts exactly the JSON grammar of [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259).
|
||||||
|
Extensions such as [comments](../features/comments.md) and [trailing commas](../features/trailing_commas.md) must be
|
||||||
|
enabled explicitly. Invalid UTF-8 is rejected.
|
||||||
|
- **Errors are reported, not ignored.** Malformed input results in a [`parse_error`](../home/exceptions.md#parse-errors)
|
||||||
|
with the byte position of the error. Binary readers do not trust announced sizes: strings and binary values grow
|
||||||
|
only as bytes are actually read, arrays reserve at most a fixed number of elements up front, and sizes that no
|
||||||
|
container can hold are rejected.
|
||||||
|
- **Memory is owned by values.** Each `basic_json` value owns its content, and there is no manual memory management in
|
||||||
|
user code. The destructor does not recurse, so destroying a deeply nested value does not exhaust the stack.
|
||||||
|
- **Bounded recursion.** The JSON parser and the binary readers keep their state in explicit stacks instead of
|
||||||
|
recursing per nesting level. Operations that walk a value, such as [`dump`](../api/basic_json/dump.md), copying,
|
||||||
|
hashing, and [`merge_patch`](../api/basic_json/merge_patch.md), recurse only up to a fixed depth and continue with an
|
||||||
|
explicit stack below it. Some operations, such as comparison, [`diff`](../api/basic_json/diff.md),
|
||||||
|
[`flatten`](../api/basic_json/flatten.md), and the binary writers, still recurse once per nesting level; work on them
|
||||||
|
is in progress. Applications that process untrusted input can limit its nesting depth with a
|
||||||
|
[parser callback](../features/parsing/parser_callbacks.md).
|
||||||
|
- **Invariants are checked.** The class invariant (for instance, that the pointer for the stored type is never null) is
|
||||||
|
checked with runtime assertions throughout the test suite.
|
||||||
|
|
||||||
|
## Common weaknesses
|
||||||
|
|
||||||
|
The following table maps the relevant classes of the [Common Weakness Enumeration](https://cwe.mitre.org) to the
|
||||||
|
measures that counter them. The measures are described in detail in [Quality assurance](quality_assurance.md).
|
||||||
|
|
||||||
|
| Weakness | Countermeasures |
|
||||||
|
|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
|
||||||
|
| Out-of-bounds read/write ([CWE-125](https://cwe.mitre.org/data/definitions/125.html), [CWE-787](https://cwe.mitre.org/data/definitions/787.html)) | bounds checks on all reads from the input; AddressSanitizer and Valgrind on the test suite; OSS-Fuzz |
|
||||||
|
| Integer overflow ([CWE-190](https://cwe.mitre.org/data/definitions/190.html)) | UndefinedBehaviorSanitizer with integer overflow detection; Clang-Tidy; Cppcheck |
|
||||||
|
| Use after free, double free ([CWE-416](https://cwe.mitre.org/data/definitions/416.html), [CWE-415](https://cwe.mitre.org/data/definitions/415.html)) | ownership of all memory by values; AddressSanitizer and Valgrind; Clang Static Analyzer |
|
||||||
|
| Memory leaks ([CWE-401](https://cwe.mitre.org/data/definitions/401.html)) | Valgrind (Memcheck) on the test suite |
|
||||||
|
| Uncontrolled recursion ([CWE-674](https://cwe.mitre.org/data/definitions/674.html)) | iterative parser, binary readers, and destructor; bounded recursion in value operations; tests with deeply nested inputs |
|
||||||
|
| Uncontrolled resource consumption ([CWE-400](https://cwe.mitre.org/data/definitions/400.html)) | allocations based on announced sizes are capped; OSS-Fuzz with memory limits |
|
||||||
|
| Undefined behavior in general ([CWE-758](https://cwe.mitre.org/data/definitions/758.html)) | UndefinedBehaviorSanitizer; runtime assertions; Clang-Tidy, Cppcheck, Clang Static Analyzer, Infer |
|
||||||
|
|
||||||
|
In addition, every line of the library is covered by the unit tests, and all parsers are fuzz-tested around the clock
|
||||||
|
by [OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/json).
|
||||||
@@ -5,4 +5,6 @@
|
|||||||
- [Contribution Guidelines](contribution_guidelines.md) - guidelines how to contribute to this project
|
- [Contribution Guidelines](contribution_guidelines.md) - guidelines how to contribute to this project
|
||||||
- [Governance](governance.md) - the governance model of this project
|
- [Governance](governance.md) - the governance model of this project
|
||||||
- [Quality Assurance](quality_assurance.md) - how the quality of this project is assured
|
- [Quality Assurance](quality_assurance.md) - how the quality of this project is assured
|
||||||
|
- [Roadmap](roadmap.md) - what the project will and will not do
|
||||||
- [Security Policy](security_policy.md) - the security policy of the project
|
- [Security Policy](security_policy.md) - the security policy of the project
|
||||||
|
- [Assurance Case](assurance_case.md) - why the library meets its security requirements
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ Note: Some modern features (like C++20 ranges or filesystem support) may be disa
|
|||||||
- [x] The parser is tested against extensive correctness suites for JSON compliance.
|
- [x] The parser is tested against extensive correctness suites for JSON compliance.
|
||||||
- [x] In addition, the library is continuously fuzz-tested at [OSS-Fuzz](https://google.github.io/oss-fuzz/) where the
|
- [x] In addition, the library is continuously fuzz-tested at [OSS-Fuzz](https://google.github.io/oss-fuzz/) where the
|
||||||
library is checked against billions of inputs.
|
library is checked against billions of inputs.
|
||||||
|
- [x] Every crash reported by OSS-Fuzz is fixed together with a unit test that reproduces it, and the fix references
|
||||||
|
the OSS-Fuzz issue. The round-trip checks of the fuzzer drivers are also part of the unit tests. See the
|
||||||
|
[fuzz testing documentation](https://github.com/nlohmann/json/blob/develop/tests/fuzzing.md#handling-oss-fuzz-reports).
|
||||||
|
|
||||||
## Static analysis
|
## Static analysis
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
This page describes what the project intends to do, and what it does not intend to do, over the next year. Concrete
|
||||||
|
work items are tracked in the [GitHub milestones](https://github.com/nlohmann/json/milestones) and the
|
||||||
|
[issue tracker](https://github.com/nlohmann/json/issues).
|
||||||
|
|
||||||
|
## What the project will do
|
||||||
|
|
||||||
|
- **Keep the C++11 baseline.** The library will continue to compile with every
|
||||||
|
[supported C++11 compiler](https://github.com/nlohmann/json/blob/develop/README.md#supported-compilers). Features of
|
||||||
|
later standards are only used when they are guarded by the `JSON_HAS_CPP_*` macros.
|
||||||
|
- **Stay conformant to JSON.** The parser and serializer follow [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259).
|
||||||
|
Extensions such as [comments](../features/comments.md) or [trailing commas](../features/trailing_commas.md) remain
|
||||||
|
opt-in.
|
||||||
|
- **Keep the 3.x public API stable.** Releases follow [semantic versioning](https://semver.org). Changes that would
|
||||||
|
break existing code are only added behind a feature macro, so users can opt in and test their code before a next
|
||||||
|
major release.
|
||||||
|
- **Support a broad range of compilers and platforms.** The [CI](quality_assurance.md) keeps testing old and new
|
||||||
|
versions of GCC, Clang, MSVC, and other compilers on Linux, macOS, and Windows.
|
||||||
|
- **Keep the quality assurance up.** Every change keeps the test coverage at 100%, passes the static and dynamic
|
||||||
|
analysis, and is fuzz-tested by OSS-Fuzz, see [Quality assurance](quality_assurance.md).
|
||||||
|
- **Harden the library against hostile input.** Handling deeply nested values without exhausting the call stack is
|
||||||
|
ongoing work.
|
||||||
|
- **Fix bugs and security issues** reported through the issue tracker and the [security policy](security_policy.md).
|
||||||
|
|
||||||
|
## What the project will not do
|
||||||
|
|
||||||
|
- **Break the public API of version 3.x.** See the
|
||||||
|
[contribution guidelines](https://github.com/nlohmann/json/blob/develop/.github/CONTRIBUTING.md#break-the-public-api)
|
||||||
|
for what counts as a breaking change.
|
||||||
|
- **Require a newer C++ standard than C++11.**
|
||||||
|
- **Break JSON conformance** or enable non-standard extensions by default.
|
||||||
|
- **Add dependencies** or require a build step. The library remains header-only, and the single header
|
||||||
|
`json.hpp` remains a complete distribution.
|
||||||
|
- **Trade simplicity for speed or memory efficiency.** Performance improvements are welcome, but the library is not
|
||||||
|
meant to compete with the fastest JSON libraries, see [Design goals](../home/design_goals.md).
|
||||||
|
|
||||||
|
## Version 4.0
|
||||||
|
|
||||||
|
There is no decision yet on whether or when a version 4.0 with breaking changes will be released. Proposals that need
|
||||||
|
a major version, for instance stricter type conversions, are collected in issue
|
||||||
|
[#3453](https://github.com/nlohmann/json/issues/3453). Until then, such changes are only added as opt-in behavior
|
||||||
|
behind feature macros.
|
||||||
@@ -208,6 +208,16 @@ The library maps BJData types to JSON value types as follows:
|
|||||||
|
|
||||||
The mapping is **complete** in the sense that any BJData value can be converted to a JSON value.
|
The mapping is **complete** in the sense that any BJData value can be converted to a JSON value.
|
||||||
|
|
||||||
|
!!! info "Round trips"
|
||||||
|
|
||||||
|
A value returned by [`from_bjdata`](../../api/basic_json/from_bjdata.md) can be serialized with
|
||||||
|
[`to_bjdata`](../../api/basic_json/to_bjdata.md) using any combination of options and parsed back into an equal
|
||||||
|
value, and serializing that value again with the same options produces the same bytes. The exception is binary
|
||||||
|
values: they are only written as an optimized binary array (`[$B`) if Draft 3 is enabled and both `use_size` and
|
||||||
|
`use_type` are set. Otherwise, they are written as arrays of integers and parsed back as such (see the notes on
|
||||||
|
binary values above), and serializing such an array again may choose different, but equally valid, type markers.
|
||||||
|
The bytes can then differ, but parsing them again yields the same value.
|
||||||
|
|
||||||
??? example
|
??? example
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -93,8 +93,9 @@ See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md).
|
|||||||
|
|
||||||
## `JSON_NO_THREAD_LOCAL`
|
## `JSON_NO_THREAD_LOCAL`
|
||||||
|
|
||||||
When defined, the library does not use `#!cpp thread_local` storage. Copying a value then always avoids the call stack
|
When defined, the library does not use `#!cpp thread_local` storage. Copying a value and comparing two values then
|
||||||
rather than descending into a bounded number of levels first, which is slower but yields the same values.
|
always avoid the call stack rather than descending into a bounded number of levels first, which is slower but yields the
|
||||||
|
same values and the same comparisons.
|
||||||
|
|
||||||
See [full documentation of `JSON_NO_THREAD_LOCAL`](../api/macros/json_no_thread_local.md).
|
See [full documentation of `JSON_NO_THREAD_LOCAL`](../api/macros/json_no_thread_local.md).
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,125 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
!!! info
|
This page gives a high-level overview of the library's architecture. It should help new contributors to get an idea of
|
||||||
|
the used concepts and where to make changes.
|
||||||
This page is still under construction. Its goal is to provide a high-level overview of the library's architecture.
|
|
||||||
This should help new contributors to get an idea of the used concepts and where to make changes.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The main structure is class [nlohmann::basic_json](../api/basic_json/index.md).
|
The library is built around a single class template, [`nlohmann::basic_json`](../api/basic_json/index.md). A
|
||||||
|
`basic_json` value is a node in a tree of JSON values. All other components either create such a tree from an input
|
||||||
|
(parsing), write a tree to an output (serialization), or give access to it (iterators, JSON Pointer, conversions).
|
||||||
|
|
||||||
- public API
|
```mermaid
|
||||||
- container interface
|
flowchart LR
|
||||||
- iterators
|
input[/"input<br>(string, stream,<br>iterator range, file)"/]
|
||||||
|
ia["input adapter"]
|
||||||
|
lexer["lexer"]
|
||||||
|
parser["parser"]
|
||||||
|
breader["binary_reader"]
|
||||||
|
sax["SAX interface"]
|
||||||
|
value[("basic_json<br>value tree")]
|
||||||
|
serializer["serializer"]
|
||||||
|
bwriter["binary_writer"]
|
||||||
|
oa["output adapter"]
|
||||||
|
output[/"output<br>(string, stream,<br>vector)"/]
|
||||||
|
|
||||||
## Template specializations
|
input --> ia
|
||||||
|
ia --> lexer --> parser --> sax
|
||||||
|
ia --> breader --> sax
|
||||||
|
sax --> value
|
||||||
|
value --> serializer --> oa
|
||||||
|
value --> bwriter --> oa
|
||||||
|
oa --> output
|
||||||
|
```
|
||||||
|
|
||||||
- describe template parameters of `basic_json`
|
- **JSON text** is read by an [input adapter](#input-adapters), tokenized by the lexer, and turned into SAX events by
|
||||||
- [`json`](../api/json.md)
|
the parser.
|
||||||
- [`ordered_json`](../api/ordered_json.md) via [`ordered_map`](../api/ordered_map.md)
|
- **Binary formats** (BJData, BSON, CBOR, MessagePack, UBJSON) are read by an input adapter and turned into the same SAX
|
||||||
|
events by the `binary_reader`.
|
||||||
|
- A [SAX consumer](#sax-interface) receives the events. The one used by [`parse`](../api/basic_json/parse.md) builds a
|
||||||
|
`basic_json` value tree.
|
||||||
|
- The `serializer` (JSON text) or the `binary_writer` (binary formats) writes a value tree to an
|
||||||
|
[output adapter](#output-adapters).
|
||||||
|
|
||||||
|
## Source layout
|
||||||
|
|
||||||
|
The public headers are in [`include/nlohmann`](https://github.com/nlohmann/json/tree/develop/include/nlohmann):
|
||||||
|
|
||||||
|
- [`json.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/json.hpp) defines class [`basic_json`](../api/basic_json/index.md).
|
||||||
|
- [`json_fwd.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/json_fwd.hpp) contains forward declarations.
|
||||||
|
- [`adl_serializer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/adl_serializer.hpp), [`byte_container_with_subtype.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/byte_container_with_subtype.hpp), and [`ordered_map.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/ordered_map.hpp) define
|
||||||
|
[`adl_serializer`](../api/adl_serializer/index.md),
|
||||||
|
[`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md), and
|
||||||
|
[`ordered_map`](../api/ordered_map.md).
|
||||||
|
|
||||||
|
Everything else lives in [`detail/`](https://github.com/nlohmann/json/tree/develop/include/nlohmann/detail) and namespace `nlohmann::detail`, which is not part of the public API. Paths
|
||||||
|
below are relative to `include/nlohmann`.
|
||||||
|
|
||||||
|
| Component | Location |
|
||||||
|
|-----------|----------|
|
||||||
|
| Value type enumeration | [`detail/value_t.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/value_t.hpp) |
|
||||||
|
| Input adapters | [`detail/input/input_adapters.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/input_adapters.hpp) |
|
||||||
|
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp) |
|
||||||
|
| Parser | [`detail/input/parser.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/parser.hpp) |
|
||||||
|
| SAX interface and DOM builders | [`detail/input/json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp) |
|
||||||
|
| Binary format readers | [`detail/input/binary_reader.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/binary_reader.hpp) |
|
||||||
|
| JSON serializer | [`detail/output/serializer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/output/serializer.hpp), [`detail/conversions/to_chars.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/conversions/to_chars.hpp) |
|
||||||
|
| Binary format writers | [`detail/output/binary_writer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/output/binary_writer.hpp) |
|
||||||
|
| Output adapters | [`detail/output/output_adapters.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/output/output_adapters.hpp) |
|
||||||
|
| Iterators | [`detail/iterators/`](https://github.com/nlohmann/json/tree/develop/include/nlohmann/detail/iterators) |
|
||||||
|
| Conversions from/to arbitrary types | [`detail/conversions/from_json.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/conversions/from_json.hpp), [`detail/conversions/to_json.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/conversions/to_json.hpp) |
|
||||||
|
| JSON Pointer | [`detail/json_pointer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/json_pointer.hpp) |
|
||||||
|
| Exceptions | [`detail/exceptions.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/exceptions.hpp) |
|
||||||
|
| Type traits and C++ feature backports | [`detail/meta/`](https://github.com/nlohmann/json/tree/develop/include/nlohmann/detail/meta) |
|
||||||
|
| Macros | [`detail/macro_scope.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/macro_scope.hpp), [`detail/macro_unscope.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/macro_unscope.hpp), [`detail/abi_macros.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/abi_macros.hpp) |
|
||||||
|
|
||||||
|
The single-header version [`single_include/nlohmann/json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp)
|
||||||
|
is generated from these files with `make amalgamate` and must not be edited by hand.
|
||||||
|
|
||||||
|
## Template parameters
|
||||||
|
|
||||||
|
[`basic_json`](../api/basic_json/index.md) is parameterized by the types it uses to store values and to convert from and to other types:
|
||||||
|
|
||||||
|
| Template parameter | Default | Used for |
|
||||||
|
|----------------------|-----------------------------|-------------------------------------------------------------------|
|
||||||
|
| `ObjectType` | `std::map` | objects, see [`object_t`](../api/basic_json/object_t.md) |
|
||||||
|
| `ArrayType` | `std::vector` | arrays, see [`array_t`](../api/basic_json/array_t.md) |
|
||||||
|
| `StringType` | `std::string` | strings and object keys, see [`string_t`](../api/basic_json/string_t.md) |
|
||||||
|
| `BooleanType` | `bool` | Booleans, see [`boolean_t`](../api/basic_json/boolean_t.md) |
|
||||||
|
| `NumberIntegerType` | `std::int64_t` | signed integers, see [`number_integer_t`](../api/basic_json/number_integer_t.md) |
|
||||||
|
| `NumberUnsignedType` | `std::uint64_t` | unsigned integers, see [`number_unsigned_t`](../api/basic_json/number_unsigned_t.md) |
|
||||||
|
| `NumberFloatType` | `double` | floating-point numbers, see [`number_float_t`](../api/basic_json/number_float_t.md) |
|
||||||
|
| `AllocatorType` | `std::allocator` | allocating objects, arrays, strings, and binary values |
|
||||||
|
| `JSONSerializer` | `adl_serializer` | conversions from/to other types, see [`adl_serializer`](../api/adl_serializer/index.md) |
|
||||||
|
| `BinaryType` | `std::vector<std::uint8_t>` | binary values, see [`binary_t`](../api/basic_json/binary_t.md) |
|
||||||
|
| `CustomBaseClass` | `void` | an optional base class, see [`json_base_class_t`](../api/basic_json/json_base_class_t.md) |
|
||||||
|
|
||||||
|
The library provides two specializations:
|
||||||
|
|
||||||
|
- [`json`](../api/json.md) uses all default template arguments.
|
||||||
|
- [`ordered_json`](../api/ordered_json.md) uses [`ordered_map`](../api/ordered_map.md) as `ObjectType` to keep the
|
||||||
|
insertion order of object keys.
|
||||||
|
|
||||||
|
The requirements on the template arguments are listed in
|
||||||
|
[Template Parameter Requirements](../features/types/template_parameters.md).
|
||||||
|
|
||||||
## Value storage
|
## Value storage
|
||||||
|
|
||||||
Values are stored as a tagged union of [value_t](../api/basic_json/value_t.md) and json_value.
|
Each [`basic_json`](../api/basic_json/index.md) value stores its content as a tagged union: an enumeration [`value_t`](../api/basic_json/value_t.md)
|
||||||
|
names the type of the value, and a union `json_value` holds the value itself. Both are members of the nested struct
|
||||||
|
`data`, which is the only data member `m_data` of `basic_json`:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
/// the type of the current element
|
struct data
|
||||||
value_t m_type = value_t::null;
|
{
|
||||||
|
/// the type of the current element
|
||||||
|
value_t m_type = value_t::null;
|
||||||
|
|
||||||
/// the value of the current element
|
/// the value of the current element
|
||||||
json_value m_value = {};
|
json_value m_value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
data m_data = {};
|
||||||
```
|
```
|
||||||
|
|
||||||
with
|
with
|
||||||
@@ -68,42 +159,83 @@ union json_value {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parsing inputs (deserialization)
|
Objects, arrays, strings, and binary values are allocated on the heap with `AllocatorType`, and the union only stores a
|
||||||
|
pointer to them. This keeps a `basic_json` value small: one pointer-sized union and one byte for the type. The class
|
||||||
|
maintains the invariant that the pointer matching `m_type` is never null; `assert_invariant()` checks it with
|
||||||
|
[runtime assertions](../features/assertions.md).
|
||||||
|
|
||||||
Input is read via **input adapters** that abstract a source with a common interface:
|
## Input adapters
|
||||||
|
|
||||||
|
Input is read via **input adapters** that abstract a source. Every input adapter provides this interface:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
/// read a single character
|
/// the type of the characters in the input
|
||||||
std::char_traits<char>::int_type get_character() noexcept;
|
using char_type = ...;
|
||||||
|
|
||||||
/// read multiple characters to a destination buffer and
|
/// read a single character; returns std::char_traits<char_type>::eof() at the end of the input
|
||||||
/// returns the number of characters successfully read
|
typename std::char_traits<char_type>::int_type get_character();
|
||||||
|
|
||||||
|
/// read up to count * sizeof(T) bytes into dest and return the number of bytes read
|
||||||
|
/// (used by the binary readers)
|
||||||
template<class T>
|
template<class T>
|
||||||
std::size_t get_elements(T* dest, std::size_t count = 1);
|
std::size_t get_elements(T* dest, std::size_t count = 1);
|
||||||
```
|
```
|
||||||
|
|
||||||
List examples of input adapters.
|
The lexer detects two optional extensions at compile time. Only `iterator_input_adapter` provides them, and only for
|
||||||
|
random-access input of single-byte characters:
|
||||||
|
|
||||||
## SAX Interface
|
- `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.
|
||||||
|
- `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in
|
||||||
|
contiguous memory, several bytes at a time.
|
||||||
|
|
||||||
TODO
|
The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the
|
||||||
|
`from_*` functions:
|
||||||
|
|
||||||
## Writing outputs (serialization)
|
- `iterator_input_adapter` reads from an iterator range, which also covers strings, containers, and pointers.
|
||||||
|
- `wide_string_input_adapter` reads from ranges of `wchar_t`, `char16_t`, or `char32_t` and converts them to UTF-8.
|
||||||
|
It cannot be used for binary formats; its `get_elements()` throws.
|
||||||
|
- `input_stream_adapter` reads from a `std::istream`.
|
||||||
|
- `file_input_adapter` reads from a `std::FILE*`.
|
||||||
|
|
||||||
|
## SAX interface
|
||||||
|
|
||||||
|
The parser does not build values itself. It reports what it reads as events to a [SAX](../features/parsing/sax_interface.md)
|
||||||
|
consumer, which implements the interface [`json_sax`](../api/json_sax/index.md): `null`, `boolean`, `number_integer`,
|
||||||
|
`number_unsigned`, `number_float`, `string`, `binary`, `start_object`, `key`, `end_object`, `start_array`, `end_array`,
|
||||||
|
and `parse_error`.
|
||||||
|
|
||||||
|
The library comes with two consumers in `detail/input/json_sax.hpp`:
|
||||||
|
|
||||||
|
- `json_sax_dom_parser` builds a [`basic_json`](../api/basic_json/index.md) value tree. [`parse`](../api/basic_json/parse.md) uses it.
|
||||||
|
- `json_sax_dom_callback_parser` does the same, but calls a [parser callback](../features/parsing/parser_callbacks.md)
|
||||||
|
for each event, which can skip values. `parse` uses it when a callback is given.
|
||||||
|
|
||||||
|
The `binary_reader` emits the same events for binary formats, so [`sax_parse`](../api/basic_json/sax_parse.md) works
|
||||||
|
with a user-defined consumer for JSON and for all binary formats alike.
|
||||||
|
|
||||||
|
## Output adapters
|
||||||
|
|
||||||
Output is written via **output adapters**:
|
Output is written via **output adapters**:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
template<typename T>
|
|
||||||
void write_character(CharType c);
|
void write_character(CharType c);
|
||||||
|
|
||||||
template<typename CharType>
|
|
||||||
void write_characters(const CharType* s, std::size_t length);
|
void write_characters(const CharType* s, std::size_t length);
|
||||||
```
|
```
|
||||||
|
|
||||||
List examples of output adapters.
|
The `serializer` (used by [`dump`](../api/basic_json/dump.md) and [`operator<<`](../api/operator_ltlt.md)) and the
|
||||||
|
`binary_writer` (used by the `to_*` functions) write to one of these adapters:
|
||||||
|
|
||||||
|
- `output_vector_adapter` appends to a `std::vector`.
|
||||||
|
- `output_stream_adapter` writes to a `std::ostream`.
|
||||||
|
- `output_string_adapter` appends to a string.
|
||||||
|
|
||||||
## Value conversion
|
## Value conversion
|
||||||
|
|
||||||
|
Values are converted from and to other types with the `JSONSerializer` template parameter. The default,
|
||||||
|
[`adl_serializer`](../api/adl_serializer/index.md), calls the free functions
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
template<class T>
|
template<class T>
|
||||||
void to_json(basic_json& j, const T& t);
|
void to_json(basic_json& j, const T& t);
|
||||||
@@ -112,13 +244,23 @@ template<class T>
|
|||||||
void from_json(const basic_json& j, T& t);
|
void from_json(const basic_json& j, T& t);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
found by argument-dependent lookup. The library defines them for standard types in `detail/conversions`; users add them
|
||||||
|
for their own types, see [Arbitrary Type Conversions](../features/arbitrary_types.md). The
|
||||||
|
[serialization macros](../features/macros.md) generate these functions.
|
||||||
|
|
||||||
## Additional features
|
## Additional features
|
||||||
|
|
||||||
- JSON Pointers
|
- [JSON Pointer](../features/json_pointer.md) (class `json_pointer`) addresses values inside a tree. It is also the
|
||||||
- Binary formats
|
basis of [JSON Patch](../features/json_patch.md).
|
||||||
- Custom base class
|
- [Binary formats](../features/binary_formats/index.md) are read by `binary_reader` and written by `binary_writer`.
|
||||||
- Conversion macros
|
- A [custom base class](../api/basic_json/json_base_class_t.md) can add members to every [`basic_json`](../api/basic_json/index.md) value.
|
||||||
|
- [Serialization macros](../features/macros.md) generate `to_json` and `from_json` functions for user-defined types.
|
||||||
|
|
||||||
## Details namespace
|
## Details namespace
|
||||||
|
|
||||||
- C++ feature backports
|
Namespace `nlohmann::detail` contains all implementation details. It is not part of the public API and may change in any
|
||||||
|
release. Besides the components above, it contains:
|
||||||
|
|
||||||
|
- type traits to detect the capabilities of user-defined types (`detail/meta/type_traits.hpp`),
|
||||||
|
- backports of C++14/17 features to C++11 (`detail/meta/cpp_future.hpp`), and
|
||||||
|
- helpers such as `string_concat` and `string_escape`.
|
||||||
|
|||||||
@@ -317,7 +317,9 @@ nav:
|
|||||||
- community/contribution_guidelines.md
|
- community/contribution_guidelines.md
|
||||||
- community/quality_assurance.md
|
- community/quality_assurance.md
|
||||||
- community/governance.md
|
- community/governance.md
|
||||||
|
- community/roadmap.md
|
||||||
- community/security_policy.md
|
- community/security_policy.md
|
||||||
|
- community/assurance_case.md
|
||||||
|
|
||||||
# Extras
|
# Extras
|
||||||
extra:
|
extra:
|
||||||
|
|||||||
+328
-20
@@ -924,6 +924,31 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief whether a descent must stop here and finish without the call stack
|
||||||
|
|
||||||
|
@a may_descend says whether the operator descends at all; it is a constant
|
||||||
|
at every call site, and is passed rather than tested by the caller so that
|
||||||
|
the test does not become a constant condition there, which MSVC reports as
|
||||||
|
C4127.
|
||||||
|
|
||||||
|
The comparison operators use this rather than @ref nesting_depth_guard::okay,
|
||||||
|
because they are written as a macro and a macro cannot use the preprocessor
|
||||||
|
the way the guard's constructor does; @ref copy_structured, which can, asks
|
||||||
|
the guard instead and never calls this.
|
||||||
|
*/
|
||||||
|
static bool nesting_depth_exhausted(bool may_descend = true) noexcept
|
||||||
|
{
|
||||||
|
#ifdef JSON_NO_THREAD_LOCAL
|
||||||
|
// without a count of its own per thread, a descent cannot be bounded
|
||||||
|
// without racing another one, so none is made
|
||||||
|
static_cast<void>(may_descend);
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return !may_descend || nesting_depth() >= nesting_depth_limit();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief counts one level of a bounded descent for as long as it runs, and
|
@brief counts one level of a bounded descent for as long as it runs, and
|
||||||
reports whether the descent was still within the limit when it began
|
reports whether the descent was still within the limit when it began
|
||||||
@@ -1243,6 +1268,274 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// the result of comparing two values, including values that cannot be
|
||||||
|
/// ordered at all, such as a discarded value or a NaN
|
||||||
|
enum class compare_result { less, equal, greater, unordered };
|
||||||
|
|
||||||
|
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||||
|
/// @brief the ordering that @a result stands for
|
||||||
|
static std::partial_ordering to_partial_ordering(compare_result result) noexcept // *NOPAD*
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case compare_result::less:
|
||||||
|
return std::partial_ordering::less;
|
||||||
|
case compare_result::greater:
|
||||||
|
return std::partial_ordering::greater;
|
||||||
|
case compare_result::equal:
|
||||||
|
return std::partial_ordering::equivalent;
|
||||||
|
case compare_result::unordered:
|
||||||
|
default:
|
||||||
|
return std::partial_ordering::unordered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief compare two values that are not both an array or both an object
|
||||||
|
|
||||||
|
Such a pair is compared by the operators themselves, which cannot descend
|
||||||
|
into it and therefore cannot recurse.
|
||||||
|
|
||||||
|
That holds for a pair whose types differ as much as for a pair of leaves: an
|
||||||
|
array and an object are told apart by their types alone, because an operator
|
||||||
|
only ever descends into two values of the same type. So `==` reports them as
|
||||||
|
unequal without looking inside either, and an ordering falls back to the
|
||||||
|
order of the types - an object sorts before an array - exactly as it does
|
||||||
|
for a value that is not nested deeply enough to get here.
|
||||||
|
*/
|
||||||
|
template<bool Ordered>
|
||||||
|
static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept
|
||||||
|
{
|
||||||
|
if (lhs == rhs)
|
||||||
|
{
|
||||||
|
return compare_result::equal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return order_leaves(lhs, rhs, std::integral_constant<bool, Ordered> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief compare two object keys
|
||||||
|
|
||||||
|
An object compares its entries as pairs of a key and a value, so its keys
|
||||||
|
are compared exactly as std::pair compares them: with < where the objects
|
||||||
|
are being ordered, and with == where they are only checked for equality.
|
||||||
|
Note that this is not the object's own comparator, which for a vector-backed
|
||||||
|
object type such as nlohmann::ordered_map tells equality rather than order.
|
||||||
|
*/
|
||||||
|
static compare_result compare_keys(const typename object_t::key_type& lhs,
|
||||||
|
const typename object_t::key_type& rhs,
|
||||||
|
std::true_type /*ordered*/)
|
||||||
|
{
|
||||||
|
if (lhs < rhs)
|
||||||
|
{
|
||||||
|
return compare_result::less;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rhs < lhs)
|
||||||
|
{
|
||||||
|
return compare_result::greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compare_result::equal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief check two object keys for equality
|
||||||
|
static compare_result compare_keys(const typename object_t::key_type& lhs,
|
||||||
|
const typename object_t::key_type& rhs,
|
||||||
|
std::false_type /*ordered*/)
|
||||||
|
{
|
||||||
|
return lhs == rhs ? compare_result::equal : compare_result::unordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief tell apart two values that are not equal
|
||||||
|
/// @note only instantiated where the values are being ordered, as a key or
|
||||||
|
/// string type is not required to be ordered to be compared for equality
|
||||||
|
static compare_result order_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept
|
||||||
|
{
|
||||||
|
if (lhs < rhs)
|
||||||
|
{
|
||||||
|
return compare_result::less;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rhs < lhs)
|
||||||
|
{
|
||||||
|
return compare_result::greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compare_result::unordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief report two values as not equal without ordering them
|
||||||
|
static compare_result order_leaves(const_reference /*lhs*/, const_reference /*rhs*/, std::false_type /*ordered*/) noexcept
|
||||||
|
{
|
||||||
|
return compare_result::unordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief compare @a lhs and @a rhs without descending into them
|
||||||
|
|
||||||
|
Reached once a comparison has descended @ref nesting_depth_limit levels, so
|
||||||
|
that comparing values cannot exhaust the call stack however deeply they are
|
||||||
|
nested. The two values are walked in lockstep on an explicit stack and
|
||||||
|
compared lexicographically, element by element in the order the containers
|
||||||
|
enumerate them - which is how the container types this library ships compare
|
||||||
|
themselves: a std::map enumerates its entries in key order, and
|
||||||
|
nlohmann::ordered_map in insertion order. An object type that enumerates its
|
||||||
|
entries in an unspecified order, such as std::unordered_map, compares them
|
||||||
|
pairwise instead; the difference could only ever show below the bound.
|
||||||
|
|
||||||
|
Note that the stack this walks with is allocated, while the comparison
|
||||||
|
operators are noexcept and the container comparison this replaces allocated
|
||||||
|
nothing. Failing that allocation therefore ends the process rather than
|
||||||
|
throwing. It only arises for values nested past the bound, and only when
|
||||||
|
memory has run out - where the same comparison used to exhaust the call
|
||||||
|
stack instead - but it is a way to fail that the operators did not have.
|
||||||
|
*/
|
||||||
|
template<bool Ordered>
|
||||||
|
static compare_result compare_iteratively(const_reference lhs, const_reference rhs,
|
||||||
|
const bool unordered_compares_equal) noexcept
|
||||||
|
{
|
||||||
|
/// a pair of containers being compared in lockstep
|
||||||
|
struct frame
|
||||||
|
{
|
||||||
|
const basic_json* lhs_value{nullptr};
|
||||||
|
const basic_json* rhs_value{nullptr};
|
||||||
|
typename array_t::const_iterator lhs_array_it{};
|
||||||
|
typename array_t::const_iterator rhs_array_it{};
|
||||||
|
typename object_t::const_iterator lhs_object_it{};
|
||||||
|
typename object_t::const_iterator rhs_object_it{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<frame> stack;
|
||||||
|
const basic_json* left = &lhs;
|
||||||
|
const basic_json* right = &rhs;
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
const auto type = left->m_data.m_type;
|
||||||
|
|
||||||
|
if (type == right->m_data.m_type && (type == value_t::array || type == value_t::object))
|
||||||
|
{
|
||||||
|
// descend: the elements decide, and are compared further down
|
||||||
|
stack.emplace_back();
|
||||||
|
frame& pushed = stack.back();
|
||||||
|
pushed.lhs_value = left;
|
||||||
|
pushed.rhs_value = right;
|
||||||
|
|
||||||
|
if (type == value_t::array)
|
||||||
|
{
|
||||||
|
pushed.lhs_array_it = left->m_data.m_value.array->cbegin();
|
||||||
|
pushed.rhs_array_it = right->m_data.m_value.array->cbegin();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pushed.lhs_object_it = left->m_data.m_value.object->cbegin();
|
||||||
|
pushed.rhs_object_it = right->m_data.m_value.object->cbegin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const compare_result result = compare_leaves<Ordered>(*left, *right);
|
||||||
|
|
||||||
|
// Values that cannot be ordered - a NaN, say - end an ordered
|
||||||
|
// comparison for std::lexicographical_compare_three_way, but
|
||||||
|
// std::lexicographical_compare treats them as equivalent and
|
||||||
|
// carries on with the next element. Both are reproduced here,
|
||||||
|
// so that a value nested too deeply to descend into compares
|
||||||
|
// exactly as one that is not.
|
||||||
|
if (result != compare_result::equal &&
|
||||||
|
!(unordered_compares_equal && result == compare_result::unordered))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk back up past the containers that are exhausted, then take the
|
||||||
|
// next pair of elements from the innermost one that is not
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (stack.empty())
|
||||||
|
{
|
||||||
|
return compare_result::equal;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame& current = stack.back();
|
||||||
|
const bool is_object = current.lhs_value->m_data.m_type == value_t::object;
|
||||||
|
|
||||||
|
const bool lhs_done = is_object
|
||||||
|
? current.lhs_object_it == current.lhs_value->m_data.m_value.object->cend()
|
||||||
|
: current.lhs_array_it == current.lhs_value->m_data.m_value.array->cend();
|
||||||
|
const bool rhs_done = is_object
|
||||||
|
? current.rhs_object_it == current.rhs_value->m_data.m_value.object->cend()
|
||||||
|
: current.rhs_array_it == current.rhs_value->m_data.m_value.array->cend();
|
||||||
|
|
||||||
|
if (lhs_done || rhs_done)
|
||||||
|
{
|
||||||
|
// whichever ran out first holds the smaller container; if
|
||||||
|
// both did, they are equal and the container above decides
|
||||||
|
if (lhs_done != rhs_done)
|
||||||
|
{
|
||||||
|
return lhs_done ? compare_result::less : compare_result::greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.pop_back();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object)
|
||||||
|
{
|
||||||
|
// an entry is a key and a value, and the key decides first
|
||||||
|
const compare_result key_result =
|
||||||
|
compare_keys(current.lhs_object_it->first, current.rhs_object_it->first,
|
||||||
|
std::integral_constant<bool, Ordered> {});
|
||||||
|
|
||||||
|
if (key_result != compare_result::equal)
|
||||||
|
{
|
||||||
|
return key_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
left = &(current.lhs_object_it->second);
|
||||||
|
right = &(current.rhs_object_it->second);
|
||||||
|
++current.lhs_object_it;
|
||||||
|
++current.rhs_object_it;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
left = &(*current.lhs_array_it);
|
||||||
|
right = &(*current.rhs_array_it);
|
||||||
|
++current.lhs_array_it;
|
||||||
|
++current.rhs_array_it;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @brief restore the parent pointers after erasing from an object
|
||||||
|
/// ordered_json keeps its members in a vector, and erasing a member
|
||||||
|
/// re-constructs every member after it in place, which resets their
|
||||||
|
/// parent pointers
|
||||||
|
void set_parents_after_object_erase()
|
||||||
|
{
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning(push )
|
||||||
|
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
|
||||||
|
#endif
|
||||||
|
if (detail::is_ordered_map<object_t>::value)
|
||||||
|
{
|
||||||
|
set_parents();
|
||||||
|
}
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning( pop )
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// JSON parser callback //
|
// JSON parser callback //
|
||||||
@@ -2931,6 +3224,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
case value_t::object:
|
case value_t::object:
|
||||||
{
|
{
|
||||||
result.m_it.object_iterator = erase_from_object(pos.m_it.object_iterator);
|
result.m_it.object_iterator = erase_from_object(pos.m_it.object_iterator);
|
||||||
|
set_parents_after_object_erase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3003,6 +3297,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
{
|
{
|
||||||
result.m_it.object_iterator = m_data.m_value.object->erase(first.m_it.object_iterator,
|
result.m_it.object_iterator = m_data.m_value.object->erase(first.m_it.object_iterator,
|
||||||
last.m_it.object_iterator);
|
last.m_it.object_iterator);
|
||||||
|
set_parents_after_object_erase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3033,7 +3328,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
|
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_data.m_value.object->erase(std::forward<KeyType>(key));
|
const auto erased = m_data.m_value.object->erase(std::forward<KeyType>(key));
|
||||||
|
set_parents_after_object_erase();
|
||||||
|
return erased;
|
||||||
}
|
}
|
||||||
|
|
||||||
template < typename KeyType, detail::enable_if_t <
|
template < typename KeyType, detail::enable_if_t <
|
||||||
@@ -3050,6 +3347,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
if (it != m_data.m_value.object->end())
|
if (it != m_data.m_value.object->end())
|
||||||
{
|
{
|
||||||
m_data.m_value.object->erase(it);
|
m_data.m_value.object->erase(it);
|
||||||
|
set_parents_after_object_erase();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -3961,16 +4259,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
if (it2 != m_data.m_value.object->end() && it2->second.is_object())
|
if (it2 != m_data.m_value.object->end() && it2->second.is_object())
|
||||||
{
|
{
|
||||||
it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1);
|
it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1);
|
||||||
#if JSON_DIAGNOSTICS
|
|
||||||
it2->second.set_parents();
|
|
||||||
#endif
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_data.m_value.object->operator[](it.key()) = it.value();
|
// set_parent() also repairs the other members, which ordered_json
|
||||||
#if JSON_DIAGNOSTICS
|
// relocates when adding a key makes its vector grow
|
||||||
m_data.m_value.object->operator[](it.key()).m_parent = this;
|
set_parent(m_data.m_value.object->operator[](it.key()) = it.value());
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3999,9 +4293,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a nested object is merged: continue with its parent
|
// a nested object is merged: continue with its parent
|
||||||
#if JSON_DIAGNOSTICS
|
|
||||||
target->set_parents();
|
|
||||||
#endif
|
|
||||||
target = stack.back().target;
|
target = stack.back().target;
|
||||||
first = stack.back().position;
|
first = stack.back().position;
|
||||||
last = stack.back().last;
|
last = stack.back().last;
|
||||||
@@ -4023,10 +4314,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target->m_data.m_value.object->operator[](first.key()) = first.value();
|
// set_parent() also repairs the other members, which ordered_json
|
||||||
#if JSON_DIAGNOSTICS
|
// relocates when adding a key makes its vector grow
|
||||||
target->m_data.m_value.object->operator[](first.key()).m_parent = target;
|
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = first.value());
|
||||||
#endif
|
|
||||||
++first;
|
++first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4165,7 +4455,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// because any negative signed value is smaller than any unsigned value.
|
// because any negative signed value is smaller than any unsigned value.
|
||||||
// Otherwise, the non-negative signed value is cast to unsigned before the
|
// Otherwise, the non-negative signed value is cast to unsigned before the
|
||||||
// comparison to avoid wraparound.
|
// comparison to avoid wraparound.
|
||||||
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \
|
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \
|
||||||
const auto lhs_type = lhs.type(); \
|
const auto lhs_type = lhs.type(); \
|
||||||
const auto rhs_type = rhs.type(); \
|
const auto rhs_type = rhs.type(); \
|
||||||
\
|
\
|
||||||
@@ -4174,10 +4464,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
switch (lhs_type) \
|
switch (lhs_type) \
|
||||||
{ \
|
{ \
|
||||||
case value_t::array: \
|
case value_t::array: \
|
||||||
|
{ \
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
|
||||||
|
{ \
|
||||||
|
return (deep_result); \
|
||||||
|
} \
|
||||||
|
const nesting_depth_guard guard; \
|
||||||
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
||||||
|
} \
|
||||||
\
|
\
|
||||||
case value_t::object: \
|
case value_t::object: \
|
||||||
|
{ \
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
|
||||||
|
{ \
|
||||||
|
return (deep_result); \
|
||||||
|
} \
|
||||||
|
const nesting_depth_guard guard; \
|
||||||
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
||||||
|
} \
|
||||||
\
|
\
|
||||||
case value_t::null: \
|
case value_t::null: \
|
||||||
return (null_result); \
|
return (null_result); \
|
||||||
@@ -4278,7 +4582,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||||
#endif
|
#endif
|
||||||
const_reference lhs = *this;
|
const_reference lhs = *this;
|
||||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false)
|
JSON_IMPLEMENT_OPERATOR( ==, true, false, false,
|
||||||
|
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
|
||||||
#ifdef __GNUC__
|
#ifdef __GNUC__
|
||||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||||
#endif
|
#endif
|
||||||
@@ -4303,7 +4608,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
||||||
std::partial_ordering::equivalent,
|
std::partial_ordering::equivalent,
|
||||||
std::partial_ordering::unordered,
|
std::partial_ordering::unordered,
|
||||||
lhs_type <=> rhs_type) // *NOPAD*
|
lhs_type <=> rhs_type, // *NOPAD*
|
||||||
|
to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @brief comparison: 3-way
|
/// @brief comparison: 3-way
|
||||||
@@ -4370,7 +4676,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
||||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||||
#endif
|
#endif
|
||||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false)
|
JSON_IMPLEMENT_OPERATOR( ==, true, false, false,
|
||||||
|
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
|
||||||
#ifdef __GNUC__
|
#ifdef __GNUC__
|
||||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||||
#endif
|
#endif
|
||||||
@@ -4426,7 +4733,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// default_result is used if we cannot compare values. In that case,
|
// default_result is used if we cannot compare values. In that case,
|
||||||
// we compare types. Note we have to call the operator explicitly,
|
// we compare types. Note we have to call the operator explicitly,
|
||||||
// because MSVC has problems otherwise.
|
// because MSVC has problems otherwise.
|
||||||
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type))
|
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type),
|
||||||
|
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @brief comparison: less than
|
/// @brief comparison: less than
|
||||||
|
|||||||
@@ -25701,6 +25701,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// @brief restore the parent pointers after erasing from an object
|
||||||
|
/// ordered_json keeps its members in a vector, and erasing a member
|
||||||
|
/// re-constructs every member after it in place, which resets their
|
||||||
|
/// parent pointers
|
||||||
|
void set_parents_after_object_erase()
|
||||||
|
{
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning(push )
|
||||||
|
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
|
||||||
|
#endif
|
||||||
|
if (detail::is_ordered_map<object_t>::value)
|
||||||
|
{
|
||||||
|
set_parents();
|
||||||
|
}
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning( pop )
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// JSON parser callback //
|
// JSON parser callback //
|
||||||
@@ -27389,6 +27410,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
case value_t::object:
|
case value_t::object:
|
||||||
{
|
{
|
||||||
result.m_it.object_iterator = erase_from_object(pos.m_it.object_iterator);
|
result.m_it.object_iterator = erase_from_object(pos.m_it.object_iterator);
|
||||||
|
set_parents_after_object_erase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27461,6 +27483,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
{
|
{
|
||||||
result.m_it.object_iterator = m_data.m_value.object->erase(first.m_it.object_iterator,
|
result.m_it.object_iterator = m_data.m_value.object->erase(first.m_it.object_iterator,
|
||||||
last.m_it.object_iterator);
|
last.m_it.object_iterator);
|
||||||
|
set_parents_after_object_erase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27491,7 +27514,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
|
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_data.m_value.object->erase(std::forward<KeyType>(key));
|
const auto erased = m_data.m_value.object->erase(std::forward<KeyType>(key));
|
||||||
|
set_parents_after_object_erase();
|
||||||
|
return erased;
|
||||||
}
|
}
|
||||||
|
|
||||||
template < typename KeyType, detail::enable_if_t <
|
template < typename KeyType, detail::enable_if_t <
|
||||||
@@ -27508,6 +27533,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
if (it != m_data.m_value.object->end())
|
if (it != m_data.m_value.object->end())
|
||||||
{
|
{
|
||||||
m_data.m_value.object->erase(it);
|
m_data.m_value.object->erase(it);
|
||||||
|
set_parents_after_object_erase();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -28419,16 +28445,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
if (it2 != m_data.m_value.object->end() && it2->second.is_object())
|
if (it2 != m_data.m_value.object->end() && it2->second.is_object())
|
||||||
{
|
{
|
||||||
it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1);
|
it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1);
|
||||||
#if JSON_DIAGNOSTICS
|
|
||||||
it2->second.set_parents();
|
|
||||||
#endif
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_data.m_value.object->operator[](it.key()) = it.value();
|
// set_parent() also repairs the other members, which ordered_json
|
||||||
#if JSON_DIAGNOSTICS
|
// relocates when adding a key makes its vector grow
|
||||||
m_data.m_value.object->operator[](it.key()).m_parent = this;
|
set_parent(m_data.m_value.object->operator[](it.key()) = it.value());
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28457,9 +28479,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a nested object is merged: continue with its parent
|
// a nested object is merged: continue with its parent
|
||||||
#if JSON_DIAGNOSTICS
|
|
||||||
target->set_parents();
|
|
||||||
#endif
|
|
||||||
target = stack.back().target;
|
target = stack.back().target;
|
||||||
first = stack.back().position;
|
first = stack.back().position;
|
||||||
last = stack.back().last;
|
last = stack.back().last;
|
||||||
@@ -28481,10 +28500,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target->m_data.m_value.object->operator[](first.key()) = first.value();
|
// set_parent() also repairs the other members, which ordered_json
|
||||||
#if JSON_DIAGNOSTICS
|
// relocates when adding a key makes its vector grow
|
||||||
target->m_data.m_value.object->operator[](first.key()).m_parent = target;
|
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = first.value());
|
||||||
#endif
|
|
||||||
++first;
|
++first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,3 +79,26 @@ the same `fuzzers` target as above and also relies on the `FUZZER_ENGINE` variab
|
|||||||
[build script](https://github.com/google/oss-fuzz/blob/master/projects/json/build.sh) for more information.
|
[build script](https://github.com/google/oss-fuzz/blob/master/projects/json/build.sh) for more information.
|
||||||
|
|
||||||
In case the build at OSS-Fuzz fails, an issue will be created automatically.
|
In case the build at OSS-Fuzz fails, an issue will be created automatically.
|
||||||
|
|
||||||
|
### Handling OSS-Fuzz reports
|
||||||
|
|
||||||
|
OSS-Fuzz files the crashes it finds in its own [issue tracker](https://issues.oss-fuzz.com), not on GitHub. So that
|
||||||
|
each report can be traced to the change that fixed it, and each fix to the report it answers, fixes follow these
|
||||||
|
conventions:
|
||||||
|
|
||||||
|
- **Reference the OSS-Fuzz issue in the pull request**, next to any GitHub issue it closes, as `OSS-Fuzz: <id>` (for
|
||||||
|
example, `OSS-Fuzz: 563659413`), and in the commit message. The ID alone does not disclose the crash. If the report
|
||||||
|
was triaged into a GitHub issue, link the OSS-Fuzz issue there too.
|
||||||
|
- **Turn the reproducer into a unit test.** Download the testcase from the OSS-Fuzz report, reduce it if possible, and
|
||||||
|
add it as a regression test to the unit test of the affected format (e.g., `tests/src/unit-bjdata.cpp`), with a
|
||||||
|
comment naming the OSS-Fuzz issue. This way the input is checked by every CI run rather than only by OSS-Fuzz, and
|
||||||
|
it stays covered even if OSS-Fuzz later closes the report as not reproducible.
|
||||||
|
- **Keep the fuzzer drivers and the unit tests in sync.** The round-trip checks of the UBJSON and BJData drivers are
|
||||||
|
also run on a fixed corpus in the unit tests (see `tests/src/round_trip_corpus.hpp` and the "round-trip invariants"
|
||||||
|
test cases), so a regression shows up in CI first. When a driver's checks change, change the unit tests with them.
|
||||||
|
- **Record in the report whether the bug shipped.** OSS-Fuzz asks whether a crash was a short-lived regression or
|
||||||
|
affects a released version; answer it when the fix is merged, as it decides whether the fix needs a release note or
|
||||||
|
a security advisory (see the [security policy](../.github/SECURITY.md)).
|
||||||
|
|
||||||
|
After the fix is merged, OSS-Fuzz re-runs the reproducer on its next build and marks the report as verified and
|
||||||
|
closed. If it does not, the fix is incomplete.
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ dump() serializes any non-finite double the same deterministic way (as JSON
|
|||||||
`null`, since JSON itself cannot represent NaN/Infinity), so comparing
|
`null`, since JSON itself cannot represent NaN/Infinity), so comparing
|
||||||
dumps is stable under exactly the same values that break operator==.
|
dumps is stable under exactly the same values that break operator==.
|
||||||
|
|
||||||
|
The unit tests run the same checks on a fixed corpus (see the "BJData round-trip
|
||||||
|
invariants" test case), so keep both in sync.
|
||||||
|
|
||||||
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
||||||
drivers.
|
drivers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ array data, it performs the following steps:
|
|||||||
- j4 = from_ubjson(vec3)
|
- j4 = from_ubjson(vec3)
|
||||||
- assert(j1 == j4)
|
- assert(j1 == j4)
|
||||||
|
|
||||||
|
The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip
|
||||||
|
invariants" test case), so keep both in sync.
|
||||||
|
|
||||||
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
|
||||||
drivers.
|
drivers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||||
|
// | | |__ | | | | | | version 3.12.0
|
||||||
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath> // nan
|
||||||
|
#include <cstddef> // size_t
|
||||||
|
#include <cstdint> // int32_t, int64_t, uint32_t, uint64_t
|
||||||
|
#include <limits> // numeric_limits
|
||||||
|
#include <random> // mt19937
|
||||||
|
#include <string> // string, to_string
|
||||||
|
#include <utility> // move
|
||||||
|
#include <vector> // vector
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
// Values for the round-trip property tests of the UBJSON and BJData writers.
|
||||||
|
//
|
||||||
|
// The fuzzer drivers (tests/src/fuzzer-parse_ubjson.cpp and
|
||||||
|
// fuzzer-parse_bjdata.cpp) check that anything the library parses can be
|
||||||
|
// serialized, parsed back, and serialized again without loss. Those checks
|
||||||
|
// only run at OSS-Fuzz, so a regression used to surface days later as an
|
||||||
|
// external report. The unit tests run the same checks on this corpus in CI.
|
||||||
|
//
|
||||||
|
// The corpus is deterministic: std::mt19937's output sequence is fixed by
|
||||||
|
// the standard, and it is used directly rather than through a distribution
|
||||||
|
// (whose results are implementation-defined).
|
||||||
|
namespace utils
|
||||||
|
{
|
||||||
|
|
||||||
|
class round_trip_corpus
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
static std::vector<json> values()
|
||||||
|
{
|
||||||
|
round_trip_corpus corpus;
|
||||||
|
return corpus.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether a value contains a binary value, which a BJData or UBJSON round
|
||||||
|
// trip may turn into an array of integers
|
||||||
|
static bool contains_binary(const json& j)
|
||||||
|
{
|
||||||
|
if (j.is_binary())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (j.is_structured())
|
||||||
|
{
|
||||||
|
for (const auto& element : j)
|
||||||
|
{
|
||||||
|
if (contains_binary(element))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<json> atoms;
|
||||||
|
// a fixed seed is the point: the corpus must be the same in every run
|
||||||
|
std::mt19937 generator{42}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||||
|
|
||||||
|
round_trip_corpus()
|
||||||
|
: atoms
|
||||||
|
{
|
||||||
|
nullptr, true, false,
|
||||||
|
// integers at the boundaries of every UBJSON/BJData integer type
|
||||||
|
0, 1, -1, 127, 128, 255, 256, -128, -129,
|
||||||
|
32767, 32768, 65535, 65536, -32768, -32769,
|
||||||
|
(std::numeric_limits<std::int32_t>::min)(), (std::numeric_limits<std::int32_t>::max)(),
|
||||||
|
(std::numeric_limits<std::uint32_t>::max)(),
|
||||||
|
(std::numeric_limits<std::int64_t>::min)(), (std::numeric_limits<std::int64_t>::max)(),
|
||||||
|
static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1u,
|
||||||
|
(std::numeric_limits<std::uint64_t>::max)(),
|
||||||
|
// floating-point numbers, including non-finite ones
|
||||||
|
0.0, -0.0, 1.5, -2.25, 3.4e38, (std::numeric_limits<double>::max)(),
|
||||||
|
std::nan(""), std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity(),
|
||||||
|
// strings, including a non-ASCII one and one longer than 255 bytes
|
||||||
|
"", "a", "\xC3\xA4", std::string(300, 'x'),
|
||||||
|
// binary values with and without subtype
|
||||||
|
json::binary({}), json::binary({1, 2, 255}), json::binary({0x80, 0x7F}, 42), json::binary({1}, 0)
|
||||||
|
}
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::vector<json> build()
|
||||||
|
{
|
||||||
|
std::vector<json> result = atoms;
|
||||||
|
|
||||||
|
// each atom inside containers, including homogeneous ones that the
|
||||||
|
// writers encode as optimized (typed) containers
|
||||||
|
result.emplace_back(json::array());
|
||||||
|
result.emplace_back(json::object());
|
||||||
|
for (const auto& atom : atoms)
|
||||||
|
{
|
||||||
|
result.push_back(json::array({atom}));
|
||||||
|
result.push_back(json::array({atom, atom, atom}));
|
||||||
|
result.push_back(json::array({json::array({atom})}));
|
||||||
|
result.push_back(json::object({{"key", atom}}));
|
||||||
|
}
|
||||||
|
result.push_back(json::array({1, 1.5}));
|
||||||
|
result.push_back(json::array({-1, 255}));
|
||||||
|
result.push_back(json::array({"a", "b"}));
|
||||||
|
|
||||||
|
// deep, but well below any recursion or depth limit
|
||||||
|
json nested_array = 1;
|
||||||
|
json nested_object = 1;
|
||||||
|
for (int i = 0; i < 300; ++i)
|
||||||
|
{
|
||||||
|
nested_array = json::array({nested_array});
|
||||||
|
nested_object = json::object({{"key", nested_object}});
|
||||||
|
}
|
||||||
|
result.push_back(nested_array);
|
||||||
|
result.push_back(nested_object);
|
||||||
|
|
||||||
|
add_annotated_arrays(result);
|
||||||
|
add_random_values(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// objects in the JData annotated array format, which the BJData writer
|
||||||
|
// encodes as ND-arrays when the annotation describes a packed array, and
|
||||||
|
// as plain objects otherwise (see #5398, #5399, #5403, #5404, and #5542)
|
||||||
|
static void add_annotated_arrays(std::vector<json>& result)
|
||||||
|
{
|
||||||
|
const std::vector<json> types =
|
||||||
|
{
|
||||||
|
"uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64",
|
||||||
|
"single", "double", "char", "byte", "bool", "unknown", 5, nullptr
|
||||||
|
};
|
||||||
|
const std::vector<json> sizes =
|
||||||
|
{
|
||||||
|
json::array(), {3}, {1, 3}, {3, 1}, {2, 3}, {2, 0}, {0, 2}, {2, 2, 2}, {-1, 2}, {2, 1.5},
|
||||||
|
"3", 3, nullptr, json::binary({})
|
||||||
|
};
|
||||||
|
const std::vector<json> data =
|
||||||
|
{
|
||||||
|
nullptr, 5, "s", json::object({{"a", 1}}), json::array(),
|
||||||
|
{1, 2, 3}, {1, 2, 3, 4, 5, 6}, {1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
{1.5, 2.5, 3.5, 4.5, 5.5, 6.5}, {300, -300, 70000, -70000, 1, 2},
|
||||||
|
{"a", "b", "c", "d", "e", "f"}, {json::array({1, 2, 3}), json::array({4, 5, 6})}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& type : types)
|
||||||
|
{
|
||||||
|
for (const auto& size : sizes)
|
||||||
|
{
|
||||||
|
for (const auto& d : data)
|
||||||
|
{
|
||||||
|
result.push_back({{"_ArrayType_", type}, {"_ArraySize_", size}, {"_ArrayData_", d}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// incomplete annotations and annotations with an extra key
|
||||||
|
result.push_back({{"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||||
|
result.push_back({{"_ArrayType_", "uint8"}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}});
|
||||||
|
result.push_back({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}});
|
||||||
|
result.push_back({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}, {"extra", 1}});
|
||||||
|
}
|
||||||
|
|
||||||
|
// random containers of atoms, both homogeneous and mixed
|
||||||
|
void add_random_values(std::vector<json>& result)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 1000; ++i)
|
||||||
|
{
|
||||||
|
result.push_back(random_value(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t random_below(std::size_t bound)
|
||||||
|
{
|
||||||
|
return generator() % bound;
|
||||||
|
}
|
||||||
|
|
||||||
|
json random_value(int depth)
|
||||||
|
{
|
||||||
|
const auto kind = random_below(10);
|
||||||
|
if (depth > 3 || kind < 5)
|
||||||
|
{
|
||||||
|
return atoms[random_below(atoms.size())];
|
||||||
|
}
|
||||||
|
|
||||||
|
json result = kind < 8 ? json::array() : json::object();
|
||||||
|
const auto count = random_below(5);
|
||||||
|
const bool homogeneous = random_below(2) == 0;
|
||||||
|
const json fixed = atoms[random_below(atoms.size())];
|
||||||
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
json element = homogeneous ? fixed : random_value(depth + 1);
|
||||||
|
if (result.is_array())
|
||||||
|
{
|
||||||
|
result.push_back(std::move(element));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result[std::to_string(i)] = std::move(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace utils
|
||||||
@@ -19,6 +19,7 @@ using nlohmann::json;
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include "make_test_data_available.hpp"
|
#include "make_test_data_available.hpp"
|
||||||
|
#include "round_trip_corpus.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -2867,6 +2868,21 @@ TEST_CASE("BJData")
|
|||||||
const auto out_num = json::to_bjdata(j_num);
|
const auto out_num = json::to_bjdata(j_num);
|
||||||
CHECK(out_num.at(0) == '{');
|
CHECK(out_num.at(0) == '{');
|
||||||
CHECK(json::from_bjdata(out_num) == j_num);
|
CHECK(json::from_bjdata(out_num) == j_num);
|
||||||
|
|
||||||
|
// OSS-Fuzz issue 474400817: an empty object _ArraySize_ was
|
||||||
|
// written as the ND-array header length, which from_bjdata()
|
||||||
|
// could not read back
|
||||||
|
const std::vector<uint8_t> input =
|
||||||
|
{
|
||||||
|
'[', '{', 'U', 11, '_', 'A', 'r', 'r', 'a', 'y', 'D', 'a', 't', 'a', '_', 'Z',
|
||||||
|
'U', 11, '_', 'A', 'r', 'r', 'a', 'y', 'T', 'y', 'p', 'e', '_', 'S', 'i', 5, 'i', 'n', 't', '1', '6',
|
||||||
|
'U', 11, '_', 'A', 'r', 'r', 'a', 'y', 'S', 'i', 'z', 'e', '_', '{', '}', '}', ']'
|
||||||
|
};
|
||||||
|
const json j1 = json::from_bjdata(input);
|
||||||
|
CHECK(j1 == json::parse(R"([{"_ArrayType_":"int16","_ArraySize_":{},"_ArrayData_":null}])"));
|
||||||
|
json j2;
|
||||||
|
CHECK_NOTHROW(j2 = json::from_bjdata(json::to_bjdata(j1, false, false)));
|
||||||
|
CHECK(j2 == j1);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("ndarray with out-of-range _ArrayData_ elements stays as object")
|
SECTION("ndarray with out-of-range _ArrayData_ elements stays as object")
|
||||||
@@ -4273,6 +4289,93 @@ TEST_CASE("BJData use_type requires use_size")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BJData round-trip invariants")
|
||||||
|
{
|
||||||
|
// This checks what the parse_bjdata_fuzzer driver checks (see
|
||||||
|
// tests/src/fuzzer-parse_bjdata.cpp), so that a regression shows up in CI
|
||||||
|
// rather than as an OSS-Fuzz report: every value from_bjdata() returns
|
||||||
|
// (j1) can be serialized with any combination of options, the result can
|
||||||
|
// be parsed back (j2), and serializing j2 again with the same options
|
||||||
|
// yields a value-equal result.
|
||||||
|
//
|
||||||
|
// Beyond the driver, this also checks that j2 equals j1 and that
|
||||||
|
// serializing j2 reproduces the exact bytes, both except for values that
|
||||||
|
// contain a binary value: a binary value is only written as a binary
|
||||||
|
// value with Draft 3's optimized binary array, and otherwise read back as
|
||||||
|
// an array of integers, for which the writer may choose different (but
|
||||||
|
// equally valid) type markers when it is serialized again (see #5494).
|
||||||
|
//
|
||||||
|
// Values are compared with dump() rather than operator==, because a NaN
|
||||||
|
// never compares equal to itself.
|
||||||
|
struct options
|
||||||
|
{
|
||||||
|
bool use_size;
|
||||||
|
bool use_type;
|
||||||
|
json::bjdata_version_t version;
|
||||||
|
};
|
||||||
|
const std::vector<options> all_options =
|
||||||
|
{
|
||||||
|
{false, false, json::bjdata_version_t::draft2},
|
||||||
|
{true, false, json::bjdata_version_t::draft2},
|
||||||
|
{true, true, json::bjdata_version_t::draft2},
|
||||||
|
{false, false, json::bjdata_version_t::draft3},
|
||||||
|
{true, false, json::bjdata_version_t::draft3},
|
||||||
|
{true, true, json::bjdata_version_t::draft3},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& j0 : utils::round_trip_corpus::values())
|
||||||
|
{
|
||||||
|
// turn the corpus value into a value as from_bjdata() returns it
|
||||||
|
for (const auto& initial : all_options)
|
||||||
|
{
|
||||||
|
const json j1 = json::from_bjdata(json::to_bjdata(j0, initial.use_size, initial.use_type, initial.version));
|
||||||
|
const bool has_binary = utils::round_trip_corpus::contains_binary(j1);
|
||||||
|
|
||||||
|
for (const auto& o : all_options)
|
||||||
|
{
|
||||||
|
INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type
|
||||||
|
<< ", draft3 = " << (o.version == json::bjdata_version_t::draft3));
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> vec = json::to_bjdata(j1, o.use_size, o.use_type, o.version);
|
||||||
|
json j2;
|
||||||
|
// anything the library writes must be parsable by the library
|
||||||
|
REQUIRE_NOTHROW(j2 = json::from_bjdata(vec));
|
||||||
|
const std::vector<std::uint8_t> vec2 = json::to_bjdata(j2, o.use_size, o.use_type, o.version);
|
||||||
|
CHECK(json::from_bjdata(vec2).dump() == j2.dump());
|
||||||
|
|
||||||
|
if (!has_binary)
|
||||||
|
{
|
||||||
|
CHECK(j2.dump() == j1.dump());
|
||||||
|
CHECK(vec2 == vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BJData round trip of a binary value is value-stable, not byte-stable")
|
||||||
|
{
|
||||||
|
// OSS-Fuzz issue 474480402: a Draft 3 optimized binary array is read as a
|
||||||
|
// binary value, which to_bjdata() writes in the default Draft 2 mode as a
|
||||||
|
// plain array of uint8 numbers. That is read back as an array of numbers,
|
||||||
|
// for which the writer then picks the smallest type marker, int8 ('i'),
|
||||||
|
// so re-serializing changes the bytes, but not the value. This is the
|
||||||
|
// exception described in the "Round trips" note of the BJData
|
||||||
|
// documentation, and why the fuzzer checks value stability (see #5494).
|
||||||
|
const std::vector<uint8_t> input = {'[', '$', 'B', '#', 'U', 1, 0x20};
|
||||||
|
const json j1 = json::from_bjdata(input);
|
||||||
|
CHECK(j1 == json::binary({0x20}));
|
||||||
|
|
||||||
|
const std::vector<uint8_t> vec = json::to_bjdata(j1, false, false);
|
||||||
|
CHECK(vec == std::vector<uint8_t>({'[', 'U', 0x20, ']'}));
|
||||||
|
const json j2 = json::from_bjdata(vec);
|
||||||
|
CHECK(j2 == json::array({0x20}));
|
||||||
|
|
||||||
|
const std::vector<uint8_t> vec2 = json::to_bjdata(j2, false, false);
|
||||||
|
CHECK(vec2 == std::vector<uint8_t>({'[', 'i', 0x20, ']'}));
|
||||||
|
CHECK(json::from_bjdata(vec2) == j2);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("BJData roundtrips" * doctest::skip())
|
TEST_CASE("BJData roundtrips" * doctest::skip())
|
||||||
{
|
{
|
||||||
SECTION("input from self-generated BJData files")
|
SECTION("input from self-generated BJData files")
|
||||||
|
|||||||
@@ -361,6 +361,106 @@ TEST_CASE("Regression tests for extended diagnostics")
|
|||||||
CHECK(p == o);
|
CHECK(p == o);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("Regression test - erase() and update() must keep JSON_DIAGNOSTICS parent pointers of ordered_json members")
|
||||||
|
{
|
||||||
|
// ordered_json keeps its members in a vector: erasing a member
|
||||||
|
// re-constructs all members after it in place, and adding a key may
|
||||||
|
// reallocate the vector; both reset the parent pointers of the members
|
||||||
|
// that were moved
|
||||||
|
using nlohmann::ordered_json;
|
||||||
|
|
||||||
|
const auto check_parents = [](const ordered_json & j)
|
||||||
|
{
|
||||||
|
// const access, so operator[] cannot repair the parent pointers
|
||||||
|
CHECK_THROWS_WITH_AS(j["z"]["x"].at(0), "[json.exception.type_error.304] (/z/x) cannot use at() with number", ordered_json::type_error);
|
||||||
|
|
||||||
|
// must not trigger assert_invariant() in a debug/assert-enabled build
|
||||||
|
ordered_json const copy = j; // NOLINT(performance-unnecessary-copy-initialization)
|
||||||
|
CHECK(copy == j);
|
||||||
|
};
|
||||||
|
|
||||||
|
// erase(key)
|
||||||
|
{
|
||||||
|
ordered_json j = {{"a", 1}, {"z", {{"x", 1}}}};
|
||||||
|
CHECK(j.erase("a") == 1);
|
||||||
|
check_parents(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// erase(iterator)
|
||||||
|
{
|
||||||
|
ordered_json j = {{"a", 1}, {"z", {{"x", 1}}}};
|
||||||
|
j.erase(j.begin());
|
||||||
|
check_parents(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// erase(iterator, iterator)
|
||||||
|
{
|
||||||
|
ordered_json j = {{"a", 1}, {"b", 2}, {"z", {{"x", 1}}}};
|
||||||
|
j.erase(j.begin(), j.find("z"));
|
||||||
|
check_parents(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// patch() removes via erase(iterator)
|
||||||
|
{
|
||||||
|
ordered_json j = {{"a", 1}, {"z", {{"x", 1}}}};
|
||||||
|
j.patch_inplace(ordered_json::parse(R"([{"op": "remove", "path": "/a"}])"));
|
||||||
|
check_parents(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update(j)
|
||||||
|
{
|
||||||
|
ordered_json j = {{"z", {{"x", 1}}}};
|
||||||
|
j.update({{"a", 1}, {"b", 2}});
|
||||||
|
check_parents(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update(j, true), the outer and the nested vector both grow
|
||||||
|
{
|
||||||
|
ordered_json j = {{"z", {{"x", 1}}}};
|
||||||
|
j.update({{"z", {{"y", 2}}}, {"a", 1}}, true);
|
||||||
|
check_parents(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update(j, true) around its descent bound, where the nested vectors
|
||||||
|
// grow while the objects are merged without recursing
|
||||||
|
for (const std::size_t depth :
|
||||||
|
{
|
||||||
|
nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), nlohmann::detail::recursion_depth_limit() + 2
|
||||||
|
})
|
||||||
|
{
|
||||||
|
ordered_json j = {{"z", {{"x", 1}}}};
|
||||||
|
ordered_json patch = {{"a", 1}, {"b", 2}, {"c", {{"d", 3}}}};
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
j = ordered_json{{"k", 0}, {"n", std::move(j)}};
|
||||||
|
patch = ordered_json{{"n", std::move(patch)}, {"l", 1}, {"m", 2}};
|
||||||
|
}
|
||||||
|
j.update(patch, true);
|
||||||
|
|
||||||
|
// must not trigger assert_invariant() on any level in a
|
||||||
|
// debug/assert-enabled build
|
||||||
|
ordered_json const copy = j; // NOLINT(performance-unnecessary-copy-initialization)
|
||||||
|
CHECK(copy == j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge_patch() inserts "c" and removes "d" at /a/c, then inserts "e"
|
||||||
|
// at /a, which copies /a/c
|
||||||
|
{
|
||||||
|
auto j = ordered_json::parse(R"({"a": {"c": {"d": {}}}})");
|
||||||
|
j.merge_patch(ordered_json::parse(R"({"a": {"c": {"c": "s", "d": null}, "e": "s"}})"));
|
||||||
|
CHECK(j.dump() == R"({"a":{"c":{"c":"s"},"e":"s"}})");
|
||||||
|
|
||||||
|
auto const& constJ = j;
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
CHECK_THROWS_WITH_AS(constJ["a"]["c"]["c"].at(0), "[json.exception.type_error.304] (/a/c/c) (bytes 18-21) cannot use at() with string", ordered_json::type_error);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(constJ["a"]["c"]["c"].at(0), "[json.exception.type_error.304] (/a/c/c) cannot use at() with string", ordered_json::type_error);
|
||||||
|
#endif
|
||||||
|
ordered_json const copy = j;
|
||||||
|
CHECK(copy == j);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()")
|
TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()")
|
||||||
|
|||||||
@@ -157,6 +157,54 @@ TEST_CASE("tests on deeply nested JSONs")
|
|||||||
CHECK(deep_depth == depth);
|
CHECK(deep_depth == depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("comparing")
|
||||||
|
{
|
||||||
|
// Comparing used to descend once per level, and an ordered
|
||||||
|
// comparison used to compare every pair of elements twice, once in
|
||||||
|
// each direction, which took exponentially long in the nesting
|
||||||
|
// depth. Both are gone: these finish in milliseconds, where the
|
||||||
|
// second used to take longer than anyone would wait even for a
|
||||||
|
// value nested only a few dozen levels deep.
|
||||||
|
const std::string text = std::string(depth, '[') + '0' + std::string(depth, ']');
|
||||||
|
const json j = json::parse(text);
|
||||||
|
const json same = json::parse(text);
|
||||||
|
const json larger = json::parse(std::string(depth, '[') + '1' + std::string(depth, ']'));
|
||||||
|
|
||||||
|
CHECK(j == same);
|
||||||
|
CHECK_FALSE(j == larger);
|
||||||
|
CHECK(j != larger);
|
||||||
|
|
||||||
|
CHECK(j < larger);
|
||||||
|
CHECK_FALSE(larger < j);
|
||||||
|
CHECK(larger > j);
|
||||||
|
CHECK(j <= same);
|
||||||
|
CHECK(j >= same);
|
||||||
|
|
||||||
|
// a value that ends earlier is the smaller one
|
||||||
|
const json shorter = json::parse(std::string(depth - 1, '[') + '0' + std::string(depth - 1, ']'));
|
||||||
|
CHECK_FALSE(j == shorter);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("comparing objects")
|
||||||
|
{
|
||||||
|
std::string text;
|
||||||
|
text.reserve((6 * depth) + 1);
|
||||||
|
for (std::size_t i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
text += "{\"a\":";
|
||||||
|
}
|
||||||
|
text += '1';
|
||||||
|
text.append(depth, '}');
|
||||||
|
|
||||||
|
const json j = json::parse(text);
|
||||||
|
const json same = json::parse(text);
|
||||||
|
|
||||||
|
CHECK(j == same);
|
||||||
|
CHECK_FALSE(j != same);
|
||||||
|
CHECK(j <= same);
|
||||||
|
CHECK(j >= same);
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("the copy is independent of the original")
|
SECTION("the copy is independent of the original")
|
||||||
{
|
{
|
||||||
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using nlohmann::json;
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include "make_test_data_available.hpp"
|
#include "make_test_data_available.hpp"
|
||||||
|
#include "round_trip_corpus.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -2265,7 +2266,9 @@ TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
|
|||||||
|
|
||||||
SECTION("an excessive count is rejected")
|
SECTION("an excessive count is rejected")
|
||||||
{
|
{
|
||||||
// 'l' is a big-endian int32: 0x7FFFFFFF elements, about 34 GB of value
|
// 'l' is a big-endian int32: 0x7FFFFFFF elements, about 34 GB of value;
|
||||||
|
// OSS-Fuzz reported this shape as a parse_ubjson_fuzzer timeout
|
||||||
|
// (testcase 6347769435193344, no issue filed)
|
||||||
for (const auto marker :
|
for (const auto marker :
|
||||||
{'Z', 'T', 'F'
|
{'Z', 'T', 'F'
|
||||||
})
|
})
|
||||||
@@ -2817,6 +2820,51 @@ TEST_CASE("UBJSON use_type requires use_size")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("UBJSON round-trip invariants")
|
||||||
|
{
|
||||||
|
// This checks what the parse_ubjson_fuzzer driver checks (see
|
||||||
|
// tests/src/fuzzer-parse_ubjson.cpp), so that a regression shows up in CI
|
||||||
|
// rather than as an OSS-Fuzz report: every value from_ubjson() returns
|
||||||
|
// (j1) can be serialized with any combination of options, the result can
|
||||||
|
// be parsed back (j2), and serializing j2 again with the same options
|
||||||
|
// reproduces the exact bytes. Beyond the driver, this also checks that j2
|
||||||
|
// equals j1. Values are compared with dump() rather than operator==,
|
||||||
|
// because a NaN never compares equal to itself.
|
||||||
|
struct options
|
||||||
|
{
|
||||||
|
bool use_size;
|
||||||
|
bool use_type;
|
||||||
|
};
|
||||||
|
const std::vector<options> all_options =
|
||||||
|
{
|
||||||
|
{false, false},
|
||||||
|
{true, false},
|
||||||
|
{true, true},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& j0 : utils::round_trip_corpus::values())
|
||||||
|
{
|
||||||
|
// turn the corpus value into a value as from_ubjson() returns it; this
|
||||||
|
// has no binary values, as UBJSON writes them as arrays of integers
|
||||||
|
for (const auto& initial : all_options)
|
||||||
|
{
|
||||||
|
const json j1 = json::from_ubjson(json::to_ubjson(j0, initial.use_size, initial.use_type));
|
||||||
|
|
||||||
|
for (const auto& o : all_options)
|
||||||
|
{
|
||||||
|
INFO("j1 = " << j1.dump() << ", use_size = " << o.use_size << ", use_type = " << o.use_type);
|
||||||
|
|
||||||
|
const std::vector<std::uint8_t> vec = json::to_ubjson(j1, o.use_size, o.use_type);
|
||||||
|
json j2;
|
||||||
|
// anything the library writes must be parsable by the library
|
||||||
|
REQUIRE_NOTHROW(j2 = json::from_ubjson(vec));
|
||||||
|
CHECK(j2.dump() == j1.dump());
|
||||||
|
CHECK(json::to_ubjson(j2, o.use_size, o.use_type) == vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("UBJSON roundtrips" * doctest::skip())
|
TEST_CASE("UBJSON roundtrips" * doctest::skip())
|
||||||
{
|
{
|
||||||
SECTION("input from self-generated UBJSON files")
|
SECTION("input from self-generated UBJSON files")
|
||||||
|
|||||||
Reference in New Issue
Block a user