Compare commits

..
Author SHA1 Message Date
Niels Lohmann bae6216491 Merge develop into remove-dead-checks/binary-formats
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 18:27:09 +01:00
Niels Lohmann 6d5581c4e8 CBOR/MessagePack readers: leave one-element arrays to the general path (#6008)
enter_cbor_array and enter_msgpack_array called the out-of-line scalar
run for every non-empty array. When the array's element is an object or
an array, as on every level of [[...]] or the single ring of a GeoJSON
polygon, the call reads nothing; it made 1,000 arrays nested in each
other cost 4 % more instructions (GCC 13) and up to 8 % more time (Apple
clang) than without the run. #5975 had fixed that with an inline
pre-check, which was later dropped after measuring canada only.

The run is now entered only for arrays of two or more elements: the same
comparison as before, so no other row changes. Arrays of one scalar,
which the run read faster, now take the general path; they are rare in
documents (one in citm_catalog, none in twitter or canada). The new
benchmark FromBinaryShape/singles measures them, next to
FromBinaryShape/nested.

Instructions per repetition, new/old, GCC 13 callgrind: nested arrays
0.973 (CBOR) and 0.971 (MessagePack), arrays of one scalar 1.037 and
1.021, every other row within 0.15 %.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 18:25:44 +01:00
Niels LohmannandClaude 590b622d48 parse(): propagate std::bad_alloc instead of crashing on a half-constructed member (#5991)
* Create the DOM parser's member value before placing it (#5985)

json_sax_dom_parser::set_member() constructed the new value in place over
the null member. basic_json(value_t) sets the type before the json_value
allocates, so a std::bad_alloc left a member typed object/array with a
null pointer, and the destructor crashed instead of propagating the
exception. Construct the value first and move it over the member (the
move constructor is noexcept).

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

* Fix CI for the parse() bad_alloc sweep test (#5985)

- sweep_allocator: provide max_size(), construct() and destroy(), which
  GCC 4.8's std::map calls on the allocator directly.
- Declare the non-throwing test lambdas noexcept (-Werror=noexcept on
  recent GCC).
- Give the sax_parse lambda an explicit return type; Clang 3.4 crashes on
  the deduced one in C++11 mode.
- Skip the parser callback sweep with iterator debugging (MSVC debug
  builds, clang-cl), where containers allocate debug proxies inside
  noexcept move constructors and terminate instead of throwing, as in the
  other allocator tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

* Construct a scalar member in place; only a throwing value goes through a temporary

Creating the value first and moving it over the member is needed only when
creating it can throw (a string, a binary value or a container). A scalar
or null is again constructed in place, without the extra move and destroy
per member (and, with JSON_DIAGNOSTICS, without the extra set_parents()).

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

* Spell the nothrow trait without && and fix the construct_member comments

The dispatch in set_member tests std::is_nothrow_constructible, not
whether the value is a scalar. Say so in the comments, and drop the &&
from the trait argument (equivalent for a forwarding reference) so that
astyle no longer inserts spaces around the template brackets.

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

* Say "for example" in the construct_member comments

The overloads are selected by std::is_nothrow_constructible, not by the
kind of value: a custom type that cannot throw takes the first one.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 18:25:30 +01:00
Niels Lohmann 100f676b3e Restore the two commits of #6005 that the stack merge dropped (fixes the build of develop) (#6012)
* basic_json, iterators, serializer: remove checks that invariants make dead

Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by the code around it:

- copy_object_level(): `src_it != src_object.cend()`; dst was built from
  exactly src_object.size() unique keys and src_it advances at most once
  per member
- the general comparison operators (equal_general, less_general,
  three_way_general): the five same-type scalar cases, which the inline
  fast path of every operator handles before calling them (now an
  assert); and the NaN half of compares_unordered() in the mixed-type
  branch, reached only after all six number pairs returned, where only
  a discarded operand is unordered
- serializer, ensure_ascii: the reset of `bytes` under error_handler_t::keep
  and the surrogate-pair escape in the UTF8_ACCEPT case; every
  well-formed multi-byte character is escaped by write_u_escapes() before
  decode() sees it, so decode() only ever accepts DEL here, a single byte
  of which nothing was buffered (now asserts)
- iter_impl(pointer): the if/else chain assigned value-initialized
  iterators to the value-initialized m_it

No change in behaviour: a build that aborts on each removed side passes
the test suite, the fuzz corpora and 23.2M dumps of all 1-, 2- and
boundary 4-byte strings in both ensure_ascii modes.

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

* iter_impl: keep the type-dependent initialization in the constructor

The check is not needed for correctness (m_it is value-initialized), but
without it GCC 13 generates slower iteration loops: a recursive walk over
a parsed document takes 0.5-2.7 % longer (callgrind: +1.9 % instructions
on twitter.json, +0.95 % on canada.json). Clang generates identical code
either way. See the measurements in #6005.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 18:23:44 +01:00
dependabot[bot] bd067fd08c Bump mkdocs-git-revision-date-localized-plugin in /docs/mkdocs (#6011)
Bumps [mkdocs-git-revision-date-localized-plugin](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin) from 1.6.0 to 1.6.2.
- [Release notes](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/releases)
- [Commits](https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/compare/v1.6.0...v1.6.2)

---
updated-dependencies:
- dependency-name: mkdocs-git-revision-date-localized-plugin
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 17:57:02 +01:00
Niels Lohmann 3354436ec4 json_view: remove checks that invariants make dead (#6006)
Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by the code around it:

- view_serializer::write_codepoint<EnsureAscii>(): the only caller
  passes the byte a run of plain output stopped at (a quote, a
  backslash or a control character), so the function is now
  write_escaped(), which writes that byte's escape sequence
- load_image(): `owned_image.empty() || image != owned_image.data()`;
  both callers hand in a fresh document_data, and for load(vector&&)
  the image is owned_image itself
- set_moved(): reserve_moved(); all three callers reserved first or
  have the moved flag set (the documented precondition)
- view_side::string() and json_side::string() in equal(): only used
  after both sides were found to be strings, so they read the string
  directly (and are noexcept)
- number<false>(): the '1'..'9' test after a sign-less dispatch, which
  only happens for a digit
- value_range(): the pre-loop `is_new || off >= size` test, which the
  loop's first pass repeats on the same node

No change in behaviour: a build that aborts on each removed side passes
the test suite and the fuzz corpora.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 17:54:40 +01:00
Niels Lohmann 596028c460 Drop the null_result parameter JSON_IMPLEMENT_OPERATOR no longer uses (#6005)
Without the same-type scalar cases, the macro body has no use for it, and
JSON_IMPLEMENT_SCALAR_CASES is now used only by JSON_IMPLEMENT_SCALAR_FAST.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 17:54:39 +01:00
Niels Lohmann 5c4f585ac2 Lexer, parser, stream adapter, SAX: remove checks that invariants make dead (#6004)
Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by the code around it:

- lexer: `next_unget` in get_codepoint_bulk() and scan_literal_bulk().
  unget() is only called at the end of scan_number(), in scan_comment()
  and in skip_bom(); every token scan starts with a plain get() in
  skip_whitespace() that consumes a pending unget, and nothing between
  that get() and the bulk helpers ungets (now asserts, as in
  get_after_string_run())
- lexer: `number_type == value_integer` in convert_integer(): both
  callers exclude value_float and the type has three values
- parser: `!context.empty()` in exception_message(): all callers pass a
  literal
- input_stream_adapter: `refill` in get_character_slow(): the function
  is only called after get_character() found the get area exhausted,
  and the variable recomputed that comparison
- json_sax_dom_callback_parser: `ref_stack.back()` in key() (the null
  case returned a few lines earlier, and the callback in between cannot
  reach ref_stack), `is_structured()` in end_object() (non-null entries
  are only pushed for objects and arrays), `!key_stack.empty()` in
  current_key() (a kept object pushed its key before any value)

No change in behaviour: a build that aborts on each removed side passes
the test suite, the fuzz corpora and 40k random documents through every
input adapter in four macro configurations.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 17:54:39 +01:00
Niels LohmannandClaude Opus 5.5 1d69ef9ab4 Raise line coverage to 99.9 %: fix the measurement, remove unreachable code, add tests (#5997)
* CI: measure line coverage without lcov's unexecuted-block adjustment

Drop geninfo_unexecuted_blocks=1 from the lcov capture of ci_test_coverage:
it reset every line of an inlined or templated function to 0 as soon as one
copy had an unexecuted block, which understated the line coverage by about
three points.

Add -fvar-tracking-assignments to both coverage builds. At -O0, GCC inlines
always_inline functions before it places the coverage counters, so the call
sites of such helpers (put_literal() and others) got no line record and
showed as 0 in an -O3 translation unit that never reaches them. With the
flag, the call sites carry counts in every translation unit. The generated
code is identical.

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

* Remove unreachable code found by the coverage analysis

Remove code that no input can reach, and mark the lines that cannot be
covered with LCOV_EXCL_LINE:

- BJData writer: the dry_run parameter of the ND-array element writers was
  always true; replace them by range checks.
- BON8 writer: call throw_integer_out_of_range() directly instead of
  write_bon8_value() for values that always throw.
- binary_writer: integer_bits() is only called for integers of up to 64 bits.
- serializer, view serializer, lexer: replace guards that the callers make
  unreachable by assertions or by the simpler code (surrogate pairs under
  ensure_ascii, next_unget, '-' at the end of the buffer).
- json_view: drop unreachable branches in scalar(), set(), check_utf8_scan(),
  append_input_impl() and unescape().
- Mark exception-cleanup braces, 2 GiB limits, big-endian and 32-bit code, and
  fall-through case labels with LCOV_EXCL_LINE.

No behavior changes for any input that reaches these functions.

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

* Tests: cover the remaining lines of the library headers

Add tests for the lines of include/nlohmann that were not covered: the
binary readers and writers, the lexer and locale paths, the json_view
modules, the custom number, key and array types, json_pointer, json_patch,
diff and the iterators.

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

* Fix test failures in the coverage tests

- unit-custom-object-key-type: remove the unused make_strict_key(strict_key)
  overload, the unused range constructor and default constructor, and guard
  data() with JSON_DIAGNOSTICS (unused-function/-member-function errors with
  GCC and clang -Weverything).
- unit-json_view_compare: move the two new sections to the new
  unit-json_view_compare_values.cpp, as the object file got too large for the
  MinGW assembler again, and use a std::map with std::greater instead of
  std::unordered_map, which does not accept the incomplete basic_json
  (clang 3.9, icpc).
- unit-json_view_dump: rename the variables that hid outer ones (MSVC C4456).
- unit-stream-bulk-scan: restore the real size of the oversized get area
  before the adapter is destroyed (JSON_PRECISE_STREAM_POSITION reads the
  character at the read position), and skip the test where the streambuf
  cannot hold a get area larger than an int (MSVC).
- unit-regression1: with JSON_NO_THREAD_LOCAL, copies are built member by
  member and keep correct timestamps, so check the corresponding outcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

* Return std::size_t from the hash helper in the ordered_json test

MSVC (Win32) rejected the implicit conversion of the unsigned long long
product to the 32-bit std::size_t (C4244 as an error). Also note when
JSON_NO_THREAD_LOCAL is set in the regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:54:38 +01:00
Niels Lohmann ff0bdfdef3 Keep the byte range in exceptions about the root value (#5990)
* Keep the byte range in exceptions about the root value (#5987)

With JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS both set, exception::diagnostics()
and the json_view diagnostics() returned early for a value without a path (the
root), dropping "(bytes a-b)". Build the "(path) " prefix only when there are
tokens and always append the byte range.

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

* Guard the null value of a value-initialized iterator; drop the unneeded found flag

With the root's byte range, diagnostics() reaches get_byte_positions() also
with JSON_DIAGNOSTICS when there is no value, as for a value-initialized
iterator (invalid_iterator.212). get_byte_positions() now returns no range
for a null value, the same change as #6003, so either can be merged first.

In the view, a value that value_path() does not find leaves no tokens, so the
found flag added nothing to the tokens.empty() test.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 17:50:40 +01:00
dependabot[bot] 7328310136 Bump mkdocs-material from 9.7.7 to 9.8.0 in /docs/mkdocs (#6010)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.7.7 to 9.8.0.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.7.7...9.8.0)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 17:48:55 +01:00
Niels LohmannandClaude Opus 5.5 bfe69c73c2 json_view: get<T>() of a boolean throws for the exact number types, as basic_json does (#5992)
* json_view: get<T>() of a boolean throws for number_integer_t, number_unsigned_t and number_float_t like basic_json (#5988)

basic_json::get<T>() of a boolean throws type_error.302 for exactly
number_integer_t, number_unsigned_t and number_float_t (dedicated from_json()
overloads) and gives 0/1 for every other arithmetic type. The view returned
0/1 for all of them, contradicting its documentation. The view now throws
the same exception (same message, with path/bytes decoration) for these three
types. basic_json is unchanged; its behavior is documented and pinned by a test.

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

* tests: skip throwing json_view bool get<T>() combinations without exceptions (#5988)

check_boolean_get_parity() called get<T>() for number_integer_t,
number_unsigned_t and number_float_t in its JSON_NOEXCEPTION branch, but a
boolean throws type_error.302 for exactly these types, so JSON_THROW aborted
test-json_view_cpp11/cpp17 in ci_test_noexceptions. Skip these combinations
there; the throwing checks stay in the exceptions-enabled build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

* Expect the byte range of the root in the json_view boolean get<T>() test

With JSON_DIAGNOSTIC_POSITIONS, the type_error.302 message starts with
"(bytes 0-4)"; build the expected text with json_view_test::message(),
as the other tests of the root value do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:48:43 +01:00
Niels LohmannandClaude 494a74c242 insert(): strong guarantee for single elements, basic guarantee for the rest (#5996)
* Make basic_json::insert() provide its documented strong exception guarantee (#5986)

A failed allocation while the new elements were copied left the value
changed, and with JSON_DIAGNOSTICS stale parent pointers behind. The
extra work now happens almost only on the failure path:

- Array insert(pos, cnt, val) and insert(pos, first, last) append the
  copies at the end and rotate them into place. If a copy throws, the
  appended elements are erased again (noexcept) and the parents are set.
  Moving a basic_json cannot throw, so reallocation is safe. This is no
  slower than vector::insert (faster for numbers), needs no temporary
  array, and keeps the elements that libstdc++ lost on reallocation.
- Array insert(pos, val) copies a non-scalar value first and inserts it
  by move, except at the end, where vector::insert is strong. Scalars
  keep the direct path.
- The object range insert(first, last) into an empty object uses the
  container's own range insertion and clears the object on an exception.
  Into a non-empty object it inserts member by member and logs one bit
  per source member (a new member is detected by the size change; no
  allocation for the first 256 members); on an exception the members
  added are erased by walking the source range. No assumption that
  key_type is string_t.
- The parents are set when a failed insertion is undone.

The regression test fails every allocation of every insert() overload in
turn, for json and ordered_json, and checks the value and its parents.

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

* Fix clang-tidy and -Wnrvo findings in insert rollback helpers

Declare the rollback helpers non-movable, replace the nested conditional
and C array in object_insert_rollback, silence the intentional copy in the
allocator test, and avoid the missed NRVO in the test factory lambda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

* insert(): strong guarantee for single elements, basic for the rest

Rework of the previous two commits, following the discussion in #5996:
the standard containers give the strong guarantee only for a single
element (std::vector: at the end, or if the exception does not come from
copying T; std::map: single-element insert), and copying a basic_json
allocates. Rebuilding a rollback for the count, range and object-range
overloads on top of array_t/object_t made the code verbose and slower.

- insert(pos, const basic_json&): at end() forward as before (the
  standard covers it); elsewhere copy first and insert by move, as
  libstdc++ does internally (libc++ shifts the tail before copying).
- insert_iterator() and object insert(first, last) set the parents in a
  scope guard, so a failed insertion leaves a valid value with
  JSON_DIAGNOSTICS too (the next assert_invariant() aborted before).
- Docs: strong guarantee for (1), basic guarantee for (2)-(5); the
  argument checks throw before anything is changed.
- Test: allocation-failure sweep checks the strong guarantee for (1) and
  validity, type and parents for the other overloads.

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

* insert(): document the exception safety of overloads 2 to 5 once

The four basic-guarantee statements were identical up to array/object.

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

* insert(): simplify the wording of the basic guarantee

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:48:29 +01:00
Niels LohmannandClaude Opus 5.5 bdc737db6d json_view: dump() without StringType::capacity() (#5999)
* json_view: dump() without StringType::capacity() (#5994)

output_buffer::grow() compared the output size with m_out.capacity(),
which the StringType requirements do not include, so dump() did not
compile with the tests' alt_string. Keep what was last reserved in the
buffer itself and use it as a lower bound of the capacity instead.

Add json_view tests with alt_string (read-only and editable documents,
dump with several growth steps, save/load), and document reserve(n) and
resize(n) as the StringType requirements of basic_json_view::dump.

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

* Use a raw string literal in the alt-string json_view test

clang-tidy (modernize-raw-string-literal) rejects the escaped literal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:48:16 +01:00
Niels LohmannandClaude 66e2eeeec0 Do not construct the base class when destroying a container (#6000)
* Do not construct the base class when destroying a container (#5995)

json_value::destroy_container() and destroy_walk() used default-
constructed basic_json values as stand-ins, which construct a
CustomBaseClass that belongs to no value. A base class whose default
constructor allocates made the noexcept destructor allocate again, and
terminate if that allocation failed, which #5762 set out to prevent.

Use data (type and value, no base class) as the stand-in instead, and
pass the children's m_data to the walk helpers.

Add a test with a counting base class: destroying a value constructs no
base object and destroys exactly one per value, also for values nested
deeper than the recursion depth limit.

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

* Release the test value with = nullptr instead of reset()

clang-tidy (readability-ambiguous-smartptr-reset-call) rejects j.reset()
because the pointee inherits a reset() from counting_base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

* Use std::swap for the container stand-in in destroy_container

The unqualified call after `using std::swap` would let a user-provided
swap(json_value&, json_value&) intercept the destruction path. Call
std::swap directly.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:48:03 +01:00
Niels LohmannandClaude Opus 5.5 03d408aafa JSON_DIAGNOSTIC_POSITIONS: comparing a value-initialized iterator throws instead of crashing (#6003)
* JSON_DIAGNOSTIC_POSITIONS: comparing a value-initialized iterator throws instead of crashing

With JSON_DIAGNOSTIC_POSITIONS, comparing a value-initialized iterator
(which has no container) with another iterator built the
invalid_iterator.212 message from the byte positions of the iterator's
value, which is a null pointer: UBSan reports "member call on null
pointer" in exception::get_byte_positions(), then the process crashes.
Every other configuration throws invalid_iterator.212.

get_byte_positions() now returns no positions for a null value, as the
JSON_DIAGNOSTICS path already does.

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

* Declare the value-initialized iterator in the test const

clang-tidy (misc-const-correctness) flags it, as it is only compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:47:48 +01:00
Niels LohmannandClaude Opus 5.5 76dbe8f9c9 Fix the red CI on develop: Visual Studio 2017 build of the BSON writer, Valgrind timeouts (#6009)
* CI: give the Valgrind tests a timeout of one hour

Under Valgrind, test-json_view_builder_cpp11 and test-ordered_json_cpp11
took 1435 s and 1390 s on develop (ddc34f5), close to ctest's default
timeout of 1500 s. On the next run (b2674a4) the whole job was about 10 %
slower and both timed out, failing ci_test_valgrind on develop and on
every PR based on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

* BSON writer: drop the SFINAE guard that Visual Studio 2017 cannot deduce

write_bson_document_single_pass() was a member template enabled by
is_detected<sink_written_t, SinkType>. MSVC 19.16 (Visual Studio 2017)
fails to deduce it through the class-scope alias template (C2672, C2783)
when it compiles to_bson() and to_bon8(), so every Visual Studio 2017
job on AppVeyor fails since #5979.

The guard is not needed: as a non-template member of the class template,
the function is only instantiated when called, and its only caller is
the write_bson_document() overload for sinks that have written(), which
already calls oa.written() and oa.rewind() itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 17:47:21 +01:00
Niels Lohmann 7503394a30 Remove is_bjd_excluded_optimized_type(), which has no caller left; fix two comments
is_valid_optimized_type() rejects the markers BJData excludes, so the reader
no longer needs its own list; the test now checks is_valid_optimized_type()
of a BJData reader against the writer's list. The BSON size comment now says
that open_bson_document() rejects a size below 5.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 09:40:51 +01:00
Niels Lohmann caabb97337 Binary reader and writer: remove checks that invariants make dead
Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by a check made
earlier on the same path:

- check_bson_document_size: `document_size < 0` (the size was rejected
  below 5 before it was stored)
- get_ubjson_ndarray_size: `second != 'N'` ('N' is rejected as a type
  marker before `second` can be set)
- bjd_type_name() == nullptr for an ndarray dtype: every marker
  is_valid_optimized_type() accepts for BJData has a name (now an assert)
- the early is_bjd_excluded_optimized_type() check in
  get_ubjson_size_type(): is_valid_optimized_type() rejects the same
  eight markers with the same error
- `input_format == bjdata` next to `is_ndarray`, bit 8 of the type, or a
  'B'/'u'/'m'/'M' marker: these only arise for BJData
- `first != npos` next to bit 8: the bit is only set after a count
- MessagePack writer: the `<= max` halves of the negative int8/16/32
  ladder (the ladder is in the `else` of `n >= 0`)

No change in behaviour: a build that aborts on each removed side passes
the test suite and the fuzz corpora, and a 1.6M-input differential run
of the reader against the original headers found no difference.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 06:58:13 +01:00
101 changed files with 3593 additions and 7892 deletions

No files matched your search

-2
View File
@@ -111,8 +111,6 @@ context which existing file needs to be extended, and only very few cases requir
When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue. When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue.
`unit-regression2.cpp` holds the older tests; the two files exist because a single one grew large enough for the `unit-regression2.cpp` holds the older tests; the two files exist because a single one grew large enough for the
MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing the larger one. MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing the larger one.
Tests that call `sax_parse` go into `unit-sax_parse.cpp` rather than into a large test file: every call instantiates
the parser and the binary reader that recover from errors, which grows a test file considerably.
#### Exceptions #### Exceptions
+2 -2
View File
@@ -495,7 +495,7 @@ bool key(string_t& val);
bool parse_error(std::size_t position, const std::string& last_token, const detail::exception& ex); bool parse_error(std::size_t position, const std::string& last_token, const detail::exception& ex);
``` ```
The return value of each function determines whether parsing should proceed. For `parse_error`, returning `true` [recovers from the error](https://json.nlohmann.me/features/parsing/error_recovery/): the parser repairs the input and continues. The return value of each function determines whether parsing should proceed.
To implement your own SAX handler, proceed as follows: To implement your own SAX handler, proceed as follows:
@@ -503,7 +503,7 @@ To implement your own SAX handler, proceed as follows:
2. Create an object of your SAX interface class, e.g. `my_sax`. 2. Create an object of your SAX interface class, e.g. `my_sax`.
3. Call `bool json::sax_parse(input, &my_sax)`; where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface. 3. Call `bool json::sax_parse(input, &my_sax)`; where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface.
Note the `sax_parse` function only returns a `bool` indicating whether the input was parsed without errors and no SAX event returned `false`. It does not return a `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error -- it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file [`json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp). Note the `sax_parse` function only returns a `bool` indicating the result of the last executed SAX event. It does not return a `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error -- it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file [`json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp).
### STL-like access ### STL-like access
+3 -3
View File
@@ -348,20 +348,20 @@ add_custom_target(ci_test_no_thread_local
add_custom_target(ci_test_coverage add_custom_target(ci_test_coverage
COMMAND CXX=g++ ${CMAKE_COMMAND} COMMAND CXX=g++ ${CMAKE_COMMAND}
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="--coverage;-fprofile-arcs;-ftest-coverage" -DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="--coverage;-fprofile-arcs;-ftest-coverage;-fvar-tracking-assignments"
-DJSON_BuildTests=ON -DJSON_BuildTests=ON
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage
COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND CXX=g++ ${CMAKE_COMMAND} COMMAND CXX=g++ ${CMAKE_COMMAND}
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="-m32;--coverage;-fprofile-arcs;-ftest-coverage" -DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="-m32;--coverage;-fprofile-arcs;-ftest-coverage;-fvar-tracking-assignments"
-DJSON_BuildTests=ON -DJSON_32bitTest=ONLY -DJSON_BuildTests=ON -DJSON_32bitTest=ONLY
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage32 -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage32
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage32 COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage32
COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage32 && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage32 && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND ${LCOV_TOOL} --directory . --capture --output-file json.info --rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1 --ignore-errors mismatch --ignore-errors unused --ignore-errors inconsistent COMMAND ${LCOV_TOOL} --directory . --capture --output-file json.info --rc branch_coverage=1 --ignore-errors mismatch --ignore-errors unused --ignore-errors inconsistent
COMMAND ${LCOV_TOOL} -e json.info ${SRC_FILES} --output-file json.info.filtered --rc branch_coverage=1 --ignore-errors unused --ignore-errors inconsistent COMMAND ${LCOV_TOOL} -e json.info ${SRC_FILES} --output-file json.info.filtered --rc branch_coverage=1 --ignore-errors unused --ignore-errors inconsistent
COMMAND ${CMAKE_SOURCE_DIR}/tests/thirdparty/imapdl/filterbr.py json.info.filtered > json.info.filtered.noexcept COMMAND ${CMAKE_SOURCE_DIR}/tests/thirdparty/imapdl/filterbr.py json.info.filtered > json.info.filtered.noexcept
COMMAND ${GENHTML_TOOL} --title "JSON for Modern C++" --legend --demangle-cpp --output-directory html --show-details --branch-coverage --ignore-errors inconsistent json.info.filtered.noexcept COMMAND ${GENHTML_TOOL} --title "JSON for Modern C++" --legend --demangle-cpp --output-directory html --show-details --branch-coverage --ignore-errors inconsistent json.info.filtered.noexcept
+3 -1
View File
@@ -178,8 +178,10 @@ function(_json_test_add_test test_name file main cxx_standard)
COMMAND ${memcheck_command} $<TARGET_FILE:${test_target}> ${DOCTEST_TEST_FILTER} COMMAND ${memcheck_command} $<TARGET_FILE:${test_target}> ${DOCTEST_TEST_FILTER}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
) )
# under Valgrind the slowest tests take close to 25 minutes, so ctest's
# default timeout of 1500 seconds leaves no headroom
set_tests_properties(${test_target}_valgrind PROPERTIES set_tests_properties(${test_target}_valgrind PROPERTIES
LABELS "valgrind" FIXTURES_REQUIRED TEST_DATA LABELS "valgrind" FIXTURES_REQUIRED TEST_DATA TIMEOUT 3600
) )
endif() endif()
endfunction() endfunction()
+10
View File
@@ -120,6 +120,16 @@ overload (3).
See [Number conversion](../../features/types/number_handling.md#number-conversion) See [Number conversion](../../features/types/number_handling.md#number-conversion)
for more information. for more information.
!!! note "Booleans as numbers"
A JSON boolean converts to `#!cpp 0` or `#!cpp 1` for every arithmetic type **except** [`number_integer_t`](number_integer_t.md),
[`number_unsigned_t`](number_unsigned_t.md) and [`number_float_t`](number_float_t.md) (by default `#!cpp std::int64_t`,
`#!cpp std::uint64_t`, and `#!cpp double`), for which `#!cpp get<T>()` throws
[`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) ("type must be number, but is boolean"). So
`#!cpp json(true).get<int>()` is `#!cpp 1`, while `#!cpp json(true).get<std::int64_t>()` and
`#!cpp json(true).get<double>()` throw. This is the behavior of all 3.x versions;
[`basic_json_view::get<T>()`](../basic_json_view/get.md) behaves the same.
!!! note "`std::optional` conversions" !!! note "`std::optional` conversions"
Prior to version 3.13.0, `#!cpp get<std::optional<T>>()` (and other conversions to `std::optional<T>`) failed to Prior to version 3.13.0, `#!cpp get<std::optional<T>>()` (and other conversions to `std::optional<T>`) failed to
+6 -1
View File
@@ -65,7 +65,12 @@ the same index, which is now a different value.
## Exception safety ## Exception safety
Strong exception safety: if an exception occurs, the original value stays intact. Overload (1) gives the strong guarantee: if an exception is thrown, there are no changes in the JSON value.
Overloads (2) to (5) give the basic guarantee: if an exception is thrown while the elements or members are copied
(for example, `std::bad_alloc`), the JSON value may be partially modified, but it stays a valid array or object.
The exceptions listed below are thrown before the JSON value is changed.
## Exceptions ## Exceptions
+1 -4
View File
@@ -104,9 +104,7 @@ The SAX event lister must follow the interface of [`json_sax`](../json_sax/index
## Return value ## Return value
`#!cpp true` if the input was parsed without errors and no SAX event returned `#!cpp false`; `#!cpp false` otherwise. return value of the last processed SAX event
In particular, the result is `#!cpp false` for input with errors, even if the SAX parser recovered from all of them
(see [error recovery](../../features/parsing/error_recovery.md)).
## Exception safety ## Exception safety
@@ -165,7 +163,6 @@ use the same file with stdio functions, but it must not wait for another thread
- Added `ignore_trailing_commas` in version 3.13.0. - Added `ignore_trailing_commas` in version 3.13.0.
- Added `tag_handler` in version 3.13.0. - Added `tag_handler` in version 3.13.0.
- Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0. - Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0.
- Recovering from parse errors (see [`parse_error`](../json_sax/parse_error.md)) added in version 3.13.0.
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0. - Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right - `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right
after the parsed value when `strict` is `#!cpp false`. after the parsed value when `strict` is `#!cpp false`.
+2 -1
View File
@@ -11,7 +11,8 @@ For the types below, the conversion works directly on the flat index — no `Bas
- `#!cpp bool` - `#!cpp bool`
- arithmetic types other than `#!cpp bool` (for a boolean, `#!cpp 0`/`#!cpp 1`, exactly as for - arithmetic types other than `#!cpp bool` (for a boolean, `#!cpp 0`/`#!cpp 1`, exactly as for
[`BasicJsonType::get<T>()`](../basic_json/get.md)) [`BasicJsonType::get<T>()`](../basic_json/get.md#notes), except for `number_integer_t`, `number_unsigned_t` and
`number_float_t`, which throw `type_error.302` for a boolean, as `BasicJsonType::get<T>()` does)
- `#!cpp std::nullptr_t` - `#!cpp std::nullptr_t`
- `#!cpp std::basic_string<char, Traits, Alloc>` (including `string_t`) — a copy of the string - `#!cpp std::basic_string<char, Traits, Alloc>` (including `string_t`) — a copy of the string
- [`string_view_t`](index.md#member-types) — **no copy**: the returned view points into the document's - [`string_view_t`](index.md#member-types) — **no copy**: the returned view points into the document's
+1 -2
View File
@@ -7,8 +7,7 @@ struct json_sax;
This class describes the SAX interface used by [sax_parse](../basic_json/sax_parse.md). Each function is called in This class describes the SAX interface used by [sax_parse](../basic_json/sax_parse.md). Each function is called in
different situations while the input is parsed. The boolean return value informs the parser whether to continue different situations while the input is parsed. The boolean return value informs the parser whether to continue
processing the input; for [`parse_error`](parse_error.md), it decides whether to processing the input.
[recover from the error](../../features/parsing/error_recovery.md).
For instance, parsing the JSON text `{"a": [1, true]}` triggers the following callbacks, in order: For instance, parsing the JSON text `{"a": [1, true]}` triggers the following callbacks, in order:
+2 -26
View File
@@ -21,18 +21,11 @@ A parse error occurred.
## Return value ## Return value
Whether to recover from the error: Whether parsing should proceed (**must return `#!cpp false`**).
- `#!cpp false` stops parsing.
- `#!cpp true` recovers from the error: the error is repaired and parsing continues. If that is not possible, which
happens in the binary formats when the end of the item with the error is unknown, the value read so far is completed
and parsing stops. See [error recovery](../../features/parsing/error_recovery.md) for how errors are repaired.
Either way, [`sax_parse`](../basic_json/sax_parse.md) returns `#!cpp false`.
## Examples ## Examples
??? example "Example: (1) the SAX interface" ??? example
The example below shows how the SAX interface is used. The example below shows how the SAX interface is used.
@@ -46,30 +39,13 @@ Either way, [`sax_parse`](../basic_json/sax_parse.md) returns `#!cpp false`.
--8<-- "examples/sax_parse.output" --8<-- "examples/sax_parse.output"
``` ```
??? example "Example: (2) recovering from errors"
The example below shows how a SAX parser recovers from errors.
```cpp
--8<-- "examples/sax_parse__error_recovery.cpp"
```
Output:
```
--8<-- "examples/sax_parse__error_recovery.output"
```
## See also ## See also
- [`sax_parse`](../basic_json/sax_parse.md) - SAX parser - [`sax_parse`](../basic_json/sax_parse.md) - SAX parser
- [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - the article on handling parse errors without - [Parsing and Exceptions](../../features/parsing/parse_exceptions.md) - the article on handling parse errors without
exceptions exceptions
- [Error Recovery](../../features/parsing/error_recovery.md) - the article on recovering from parse errors
- [SAX Interface](../../features/parsing/sax_interface.md) - guide to event-based parsing - [SAX Interface](../../features/parsing/sax_interface.md) - guide to event-based parsing
## Version history ## Version history
- Added in version 3.2.0. - Added in version 3.2.0.
- Returning `#!cpp true` recovers from the error since version 3.13.0; before, parsing stopped, but the result of
[`sax_parse`](../basic_json/sax_parse.md) could be wrong.
@@ -35,6 +35,9 @@ Note that enabling this macro increases the size of every JSON value by two `std
overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions. It also causes overhead to parsing, copying JSON value objects, and the generation of error messages for exceptions. It also causes
these values to be reported in those error messages. these values to be reported in those error messages.
If [`JSON_DIAGNOSTICS`](json_diagnostics.md) is also set, the message contains the path and the byte range, such as
`(/1) (bytes 4-8)`. An exception about the root value has no path and only the range, such as `(bytes 0-4)`.
## Default definition ## Default definition
The default value is `0` (position diagnostics are switched off). The default value is `0` (position diagnostics are switched off).
@@ -1,43 +0,0 @@
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// a SAX parser that creates a JSON value like json::parse does, but that
// recovers from parse errors instead of stopping at the first one
class recovering_parser : public nlohmann::detail::json_sax_dom_parser<json>
{
public:
explicit recovering_parser(json& result)
: nlohmann::detail::json_sax_dom_parser<json>(result, false)
{}
bool parse_error(std::size_t position,
const std::string& /*last_token*/,
const json::exception& ex)
{
std::cout << "byte " << position << ": " << ex.what() << '\n';
// repair the input and continue
return true;
}
};
int main()
{
// JSON text with several mistakes that ends too early
const std::string text = R"({
"name": "Hello World",
"tags": ["a" "b",],
"valid": tru,
"size": 1.,
"nested": {"x": 1)";
json result;
recovering_parser sax(result);
const bool valid = json::sax_parse(text, &sax);
std::cout << "\nvalid JSON: " << std::boolalpha << valid << '\n'
<< std::setw(4) << result << std::endl;
}
@@ -1,19 +0,0 @@
byte 49: [json.exception.parse_error.101] parse error at line 3, column 20: syntax error while parsing array - unexpected string literal; expected ']'
byte 51: [json.exception.parse_error.101] parse error at line 3, column 22: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal
byte 70: [json.exception.parse_error.101] parse error at line 4, column 17: syntax error while parsing value - invalid literal; last read: '"valid": tru,'
byte 86: [json.exception.parse_error.101] parse error at line 5, column 15: syntax error while parsing value - invalid number; expected digit after '.'; last read: '1.,'
byte 109: [json.exception.parse_error.101] parse error at line 6, column 22: syntax error while parsing object - unexpected end of input; expected '}'
valid JSON: false
{
"name": "Hello World",
"nested": {
"x": 1
},
"size": 1,
"tags": [
"a",
"b"
],
"valid": null
}
@@ -1,122 +0,0 @@
# Error Recovery
By default, parsing stops at the first error. With the [SAX interface](sax_interface.md), you can instead ask the
parser to *recover*: to repair the error and continue, so that you get as much as possible out of malformed input, for
instance a file that was cut off, JSON edited by hand, or the output of a language model.
## Recovering from errors
The SAX parser's [`parse_error`](../../api/json_sax/parse_error.md) function is called for every error. Its return value
decides what happens next:
- `#!cpp false` stops parsing. This is what the SAX parsers of the library do, so [`parse`](../../api/basic_json/parse.md)
and [`accept`](../../api/basic_json/accept.md) never recover.
- `#!cpp true` repairs the error and continues parsing.
When recovering, the SAX parser still receives well-formed events: every `start_object` or `start_array` is followed by
the matching `end_object` or `end_array`, and every `key` is followed by exactly one value. A SAX parser that creates a
JSON value, such as the one in the example below, therefore gets a complete value. Parsing always ends, and
[`sax_parse`](../../api/basic_json/sax_parse.md) returns `#!cpp false` for input that is not valid JSON, even if every
error was repaired. Each token is reported at most once, and the SAX parser can stop at any error by returning
`#!cpp false`.
!!! example
The example below derives a SAX parser from the library's parser for `json` values (`json_sax_dom_parser`),
and recovers from all errors.
```cpp
--8<-- "examples/sax_parse__error_recovery.cpp"
```
Output:
```
--8<-- "examples/sax_parse__error_recovery.output"
```
## How errors are repaired
Each error is repaired with the smallest local edit: a missing separator is inserted, a stray token is removed, what can
be read of a broken string or number is kept, and a value that cannot be read at all becomes `#!json null`.
| Mistake | Repair | Example | Result |
|---------------------------|--------------------------------------------------------------------------------|------------------------------------------|----------------------------|
| missing `,` or `:` | inserted | `#!json [1 2]`, `#!json {"a" 1}` | `[1,2]`, `{"a":1}` |
| missing value | `#!json null` for an object key or between commas in an array | `#!json {"a":}`, `#!json [1,,2]` | `{"a":null}`, `[1,null,2]` |
| trailing comma | removed | `#!json [1,2,]` | `[1,2]` |
| broken string | invalid escapes and bytes are replaced (see below); a line break ends the string | `#!json ["a\qb"]` | `["aqb"]` |
| broken number | the longest valid beginning is kept | `#!json [1., 2e+]` | `[1,2]` |
| unreadable value | `#!json null` | `#!json [1, NaN, tru]` | `[1,null,null]` |
| number too large | passed as infinity, together with its text | `#!json [1e999]` | infinity (see below) |
| stray `:` | removed | `#!json ["a":1]` | `["a",1]` |
| member without a key | skipped up to the next `,` or `}` | `#!json {1:2, "b":3}` | `{"b":3}` |
| wrong closing bracket | closes the innermost array or object | `#!json {"a":[1,2}, "b":3}` | `{"a":[1,2],"b":3}` |
| input ends too early | all open arrays and objects are closed | `#!json {"a":[1,2` | `{"a":[1,2]}` |
| text before the value | skipped | `#!json )]}'{"a":1}` | `{"a":1}` |
In a string, an unknown escape like `\q` stands for the escaped character (`q`), as in JavaScript. An invalid `\u`
escape, a lone surrogate, and ill-formed UTF-8 are each replaced by U+FFFD (REPLACEMENT CHARACTER), and control
characters are kept. A string without its closing quote ends at the next line break or at the end of the input.
The input after the top-level value is not repaired: as without recovery, it is reported as an error, and parsing stops.
## Binary formats
The binary formats ([BJData](../binary_formats/bjdata.md), [BON8](../binary_formats/bon8.md),
[BSON](../binary_formats/bson.md), [CBOR](../binary_formats/cbor.md), [MessagePack](../binary_formats/messagepack.md),
and [UBJSON](../binary_formats/ubjson.md)) have no delimiters to find the next value by. So what can be repaired depends
on whether the end of the item with the error is known, a distinction that
[RFC 8949, Section 5.3](https://www.rfc-editor.org/rfc/rfc8949.html#section-5.3) makes for CBOR, too.
If the item is complete, but cannot be passed on as it is, it is replaced, and parsing continues after it:
| Mistake | Formats | Repair |
|---------------------------------------------------------------------|-------------------------|-------------------------------------------------------------------------|
| tag, if `tag_handler` is `cbor_tag_handler_t::error` (the default) | CBOR | ignored |
| simple value other than `false`, `true`, and `null`, like undefined | CBOR | `#!json null` |
| number too large for a custom `number_float_t`, like `float` | all | infinity |
| character (`C`) that is not ASCII | BJData, UBJSON | U+FFFD |
| invalid high-precision number (`H`) | BJData, UBJSON | the longest valid beginning is kept, as for JSON text, or `#!json null` |
| high-precision number too large | BJData, UBJSON | passed as infinity, together with its text |
| object key that is not a string | BON8, CBOR, MessagePack | the member is skipped |
| element of a type the library does not read, like ObjectId or date | BSON | `#!json null` |
| string without its terminator | BSON | kept |
| document whose size does not match its content | BSON | kept |
CBOR tags and simple values are repaired as [RFC 8949, Section 6.1](https://www.rfc-editor.org/rfc/rfc8949.html#section-6.1)
suggests for converting CBOR to JSON. By default, [`sax_parse`](../../api/basic_json/sax_parse.md) reports every tag as
an error; when recovering, tags are then ignored like with
[`cbor_tag_handler_t::ignore`](../../api/basic_json/cbor_tag_handler_t.md). Strings that are not valid UTF-8 are no
error: like [`from_cbor`](../../api/basic_json/from_cbor.md) and the other functions by default, `sax_parse` passes
them on as they are.
After any other error, the end of the item is unknown: the input ended, a byte is not a valid type marker, or a size
cannot be right. Parsing then stops, and the value read so far is completed: a key that waits for its value gets
`#!json null`, and all open arrays and objects are closed. This keeps everything before the error of an input that was
cut off. The exception is BSON, which stores the size of every document: an element whose end is unknown gets
`#!json null`, the rest of its document is skipped, and parsing continues after the document.
## Limitations
- A repair is a guess. For example, `#!json {"a" "b": 1}` could be meant as `#!json {"a": "b"}` or as
`#!json {"a": null, "b": 1}`; it is repaired to the former. Treat recovered values as a best effort, and check the
reported errors.
- A closing bracket always closes the innermost array or object. If a bracket is missing rather than wrong, the
repair differs from the intention: `#!json {"a": {"b": [1, 2}, "c": 3}` is repaired to
`#!json {"a": {"b": [1, 2], "c": 3}}`, although `#!json {"a": {"b": [1, 2]}, "c": 3}` may have been meant.
- Keys without quotes, and strings in single quotes, are not supported; such members are skipped.
- In the binary formats, a member that is skipped because its key is not a string is lost, and so are the elements of a
BSON document after one whose end is unknown.
- A number that is too large for `number_float_t` is passed as positive or negative infinity. The SAX parser's
`number_float` also gets the number's text, but a JSON value cannot store it, and
[`dump`](../../api/basic_json/dump.md) serializes infinity as `#!json null`.
- When parsing is not strict (see [`sax_parse`](../../api/basic_json/sax_parse.md)), a repair may read parts of the
input after the value, for instance of the next value in a stream of concatenated values.
## See also
- [SAX interface](sax_interface.md) - implement a custom SAX handler
- [`parse_error`](../../api/json_sax/parse_error.md) - the SAX event for parse errors
- [`sax_parse`](../../api/basic_json/sax_parse.md) - generate SAX events
- [parsing and exceptions](parse_exceptions.md) - control error handling
+1 -2
View File
@@ -141,7 +141,7 @@ See [Zero-copy JSON views](../json_view.md) for the concept and the trade-offs.
When the input is not valid JSON, the `parse` function throws an exception by default. If exceptions are undesired or When the input is not valid JSON, the `parse` function throws an exception by default. If exceptions are undesired or
unavailable, the parser can instead return a discarded value, or [`accept`](../../api/basic_json/accept.md) can be used unavailable, the parser can instead return a discarded value, or [`accept`](../../api/basic_json/accept.md) can be used
to only check whether an input is valid JSON. See [parsing and exceptions](parse_exceptions.md) for the available to only check whether an input is valid JSON. See [parsing and exceptions](parse_exceptions.md) for the available
options. To get as much as possible out of malformed input, a SAX parser can [recover from errors](error_recovery.md). options.
## See also ## See also
@@ -155,7 +155,6 @@ options. To get as much as possible out of malformed input, a SAX parser can [re
- [SAX interface](sax_interface.md) - implement a custom SAX handler - [SAX interface](sax_interface.md) - implement a custom SAX handler
- [Zero-copy JSON views](../json_view.md) - parse into a read-only document and read it through views - [Zero-copy JSON views](../json_view.md) - parse into a read-only document and read it through views
- [parsing and exceptions](parse_exceptions.md) - control error handling - [parsing and exceptions](parse_exceptions.md) - control error handling
- [error recovery](error_recovery.md) - get as much as possible out of malformed input
- [parsing untrusted input](untrusted_input.md) - what to consider when parsing input from untrusted sources - [parsing untrusted input](untrusted_input.md) - what to consider when parsing input from untrusted sources
- [`operator>>`](../../api/operator_gtgt.md) - deserialize from stream - [`operator>>`](../../api/operator_gtgt.md) - deserialize from stream
- [`json_sax`](../../api/json_sax/index.md) - documentation of the SAX interface - [`json_sax`](../../api/json_sax/index.md) - documentation of the SAX interface
@@ -68,8 +68,7 @@ bool parse_error(std::size_t position,
const json::exception& ex); const json::exception& ex);
``` ```
The return value decides whether to stop parsing (`#!cpp false`) or to repair the error and continue The return value indicates whether the parsing should continue, so the function should usually return `#!cpp false`.
(`#!cpp true`); see [error recovery](error_recovery.md) for the latter.
??? example "Example: report parse errors without exceptions" ??? example "Example: report parse errors without exceptions"
@@ -62,8 +62,7 @@ bool key(string_t& val);
bool parse_error(std::size_t position, const std::string& last_token, const json::exception& ex); bool parse_error(std::size_t position, const std::string& last_token, const json::exception& ex);
``` ```
The return value of each function determines whether parsing should proceed. For `parse_error`, returning The return value of each function determines whether parsing should proceed.
`#!cpp true` [recovers from the error](error_recovery.md).
Object keys are passed to `key` decoded and validated: escape sequences are already resolved, so different spellings of Object keys are passed to `key` decoded and validated: escape sequences are already resolved, so different spellings of
the same name yield the same string. See [`key`](../../api/json_sax/key.md#notes) for the exact guarantees. the same name yield the same string. See [`key`](../../api/json_sax/key.md#notes) for the exact guarantees.
@@ -102,7 +101,7 @@ To implement your own SAX handler, proceed as follows:
2. Create an object of your SAX interface class, e.g. `my_sax`. 2. Create an object of your SAX interface class, e.g. `my_sax`.
3. Call `#!cpp bool json::sax_parse(input, &my_sax);` where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface. 3. Call `#!cpp bool json::sax_parse(input, &my_sax);` where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface.
Note the `sax_parse` function only returns a `#!cpp bool` indicating whether the input was parsed without errors and no SAX event returned `#!cpp false`. It does not return `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error - it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file `json_sax.hpp`. Note the `sax_parse` function only returns a `#!cpp bool` indicating the result of the last executed SAX event. It does not return `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error - it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file `json_sax.hpp`.
## See also ## See also
@@ -406,6 +406,7 @@ using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
| `#!cpp operator<<(std::ostream&, const json_pointer&)` | streamability to `#!cpp std::ostream` | | `#!cpp operator<<(std::ostream&, const json_pointer&)` | streamability to `#!cpp std::ostream` |
| [`to_string`](../../api/basic_json/to_string.md) | conversion of `StringType` to `#!cpp std::string` (the function returns a `#!cpp std::string`) | | [`to_string`](../../api/basic_json/to_string.md) | conversion of `StringType` to `#!cpp std::string` (the function returns a `#!cpp std::string`) |
| exception messages | `data()` and `size()`, or `begin()` and `end()` | | exception messages | `data()` and `size()`, or `begin()` and `end()` |
| [`basic_json_view::dump`](../../api/basic_json_view/dump.md) | `reserve(n)` and `resize(n)` (one argument); `capacity()` is **not** required |
### Compatible types ### Compatible types
-1
View File
@@ -142,7 +142,6 @@ nav:
- features/performance.md - features/performance.md
- Parsing: - Parsing:
- features/parsing/index.md - features/parsing/index.md
- features/parsing/error_recovery.md
- features/parsing/json_lines.md - features/parsing/json_lines.md
- features/parsing/large_and_partial_input.md - features/parsing/large_and_partial_input.md
- features/parsing/parse_exceptions.md - features/parsing/parse_exceptions.md
+2 -2
View File
@@ -1,8 +1,8 @@
wheel==0.48.0 wheel==0.48.0
mkdocs==1.6.1 # documentation framework mkdocs==1.6.1 # documentation framework
mkdocs-git-revision-date-localized-plugin==1.6.0 # plugin "git-revision-date-localized" mkdocs-git-revision-date-localized-plugin==1.6.2 # plugin "git-revision-date-localized"
mkdocs-material==9.7.7 # theme for mkdocs mkdocs-material==9.8.0 # theme for mkdocs
mkdocs-material-extensions==1.3.1 # extensions mkdocs-material-extensions==1.3.1 # extensions
mkdocs-minify-plugin==0.8.0 # plugin "minify" mkdocs-minify-plugin==0.8.0 # plugin "minify"
mkdocs-redirects==1.2.3 # plugin "redirects" mkdocs-redirects==1.2.3 # plugin "redirects"
@@ -91,7 +91,7 @@ const typename BasicJsonType::binary_t& binary_value(const BasicJsonType& j)
} }
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0> template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) // LCOV_EXCL_LINE
{ {
return {}; // LCOV_EXCL_LINE return {}; // LCOV_EXCL_LINE
} }
+9 -7
View File
@@ -198,18 +198,19 @@ class exception : public std::exception
// all other parent types (primitives) contribute no token // all other parent types (primitives) contribute no token
} }
if (tokens.empty()) // the root value has no path, but still has a byte range
std::string prefix;
if (!tokens.empty())
{ {
return ""; const auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
}
auto str = std::accumulate(tokens.rbegin(), tokens.rend(), std::string{},
[](const std::string & a, const std::string & b) [](const std::string & a, const std::string & b)
{ {
return concat(a, '/', detail::escape(b)); return concat(a, '/', detail::escape(b));
}); });
prefix = concat('(', str, ") ");
}
return concat('(', str, ") ", get_byte_positions(leaf_element)); return concat(prefix, get_byte_positions(leaf_element));
#else #else
return get_byte_positions(leaf_element); return get_byte_positions(leaf_element);
#endif #endif
@@ -222,7 +223,8 @@ class exception : public std::exception
template<typename BasicJsonType> template<typename BasicJsonType>
static std::string get_byte_positions(const BasicJsonType* leaf_element) static std::string get_byte_positions(const BasicJsonType* leaf_element)
{ {
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos)) // a value-initialized iterator has no value (invalid_iterator.212)
if (leaf_element != nullptr && leaf_element->start_pos() != std::string::npos && leaf_element->end_pos() != std::string::npos)
{ {
return concat("(bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") "); return concat("(bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
} }
File diff suppressed because it is too large. Load diff
@@ -545,12 +545,9 @@ class input_stream_adapter
std::char_traits<char>::int_type get_character_slow() std::char_traits<char>::int_type get_character_slow()
{ {
#if !JSON_DISABLE_STREAM_BULK_SCAN #if !JSON_DISABLE_STREAM_BULK_SCAN
// sbumpc() refills the get area if it is exhausted // the get area is exhausted (get_character() handles the other case),
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); // so sbumpc() refills it
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment(); save_token_segment();
}
#endif #endif
auto res = sb->sbumpc(); auto res = sb->sbumpc();
@@ -563,17 +560,13 @@ class input_stream_adapter
is->clear(is->rdstate() | std::ios::eofbit); is->clear(is->rdstate() | std::ios::eofbit);
} }
#if !JSON_DISABLE_STREAM_BULK_SCAN #if !JSON_DISABLE_STREAM_BULK_SCAN
else if (JSON_HEDLEY_UNLIKELY(refill)) else
{ {
// the consumed character is at gptr() - 1, unless the streambuf // the consumed character is at gptr() - 1, unless the streambuf
// has no get area and returned it from uflow() // has no get area and returned it from uflow()
const char* const next = streambuf_access::get_ptr(sb); const char* const next = streambuf_access::get_ptr(sb);
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
} }
else
{
current_in_get_area = true;
}
#endif #endif
return res; return res;
} }
@@ -796,22 +789,17 @@ class iterator_input_adapter
return get_consumed_count() + eof_reads; return get_consumed_count() + eof_reads;
} }
// number of newlines in the consumed range [first_index, last_index); if // number of newlines in the consumed range [0, last_index); @a line_start
// there is one, @a line_start is set to the index after the last of them, // is set to the index after the last of them (0 if there is none); only
// otherwise it is left unchanged; only valid when supports_seek // valid when supports_seek
std::size_t count_consumed_lines(std::size_t first_index, std::size_t last_index, std::size_t& line_start) const std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
{ {
using difference_type = typename std::iterator_traits<IteratorType>::difference_type; const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
const auto from = std::next(begin, static_cast<difference_type>(first_index));
const auto to = std::next(from, static_cast<difference_type>(last_index - first_index));
const auto newline = char_traits<char_type>::to_char_type('\n'); const auto newline = char_traits<char_type>::to_char_type('\n');
using reverse_it = std::reverse_iterator<IteratorType>; using reverse_it = std::reverse_iterator<IteratorType>;
const auto last_newline = std::find(reverse_it(to), reverse_it(from), newline); const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
if (last_newline.base() != from)
{
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base())); line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
} return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
return static_cast<std::size_t>(std::count(from, last_newline.base(), newline));
} }
// append the already-consumed characters in the half-open range // append the already-consumed characters in the half-open range
+28 -16
View File
@@ -138,9 +138,7 @@ struct json_sax
@param[in] position the position in the input where the error occurs @param[in] position the position in the input where the error occurs
@param[in] last_token the last read token @param[in] last_token the last read token
@param[in] ex an exception object describing the error @param[in] ex an exception object describing the error
@return whether to recover from the error: false stops parsing; true @return whether parsing should proceed (must return false)
repairs the error and continues, or, if that is not possible,
stops after completing the value read so far
*/ */
virtual bool parse_error(std::size_t position, virtual bool parse_error(std::size_t position,
const std::string& last_token, const std::string& last_token,
@@ -487,9 +485,6 @@ stored in place as it arrives, so after a parse error the value holds what was
read up to it. read up to it.
@tparam BasicJsonType the JSON type @tparam BasicJsonType the JSON type
@tparam InputAdapterType the input adapter of the lexer that can be passed to
the constructor to record diagnostic positions; it
does not matter if no lexer is passed
@tparam Collect whether to collect the values of containers of unknown size on @tparam Collect whether to collect the values of containers of unknown size on
a stack (only for a caller that does not use the value after a a stack (only for a caller that does not use the value after a
parse error) parse error)
@@ -498,7 +493,7 @@ read up to it.
it, so any type with its three functions works (as it, so any type with its three functions works (as
materialize() of json_view uses it) materialize() of json_view uses it)
*/ */
template<typename BasicJsonType, typename InputAdapterType = string_input_adapter_type, bool Collect = false, typename LexerType = lexer<BasicJsonType, InputAdapterType>> template<typename BasicJsonType, typename InputAdapterType, bool Collect = false, typename LexerType = lexer<BasicJsonType, InputAdapterType>>
class json_sax_dom_parser : public json_sax_dom_error_state class json_sax_dom_parser : public json_sax_dom_error_state
{ {
public: public:
@@ -804,8 +799,6 @@ class json_sax_dom_parser : public json_sax_dom_error_state
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
void grow() void grow()
{ {
// the stack starts with local_capacity frames and only grows
JSON_ASSERT(capacity != 0);
const std::size_t new_capacity = capacity * 2; const std::size_t new_capacity = capacity * 2;
std::unique_ptr<frame[]> new_heap(new frame[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) std::unique_ptr<frame[]> new_heap(new frame[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
for (std::size_t i = 0; i < count; ++i) for (std::size_t i = 0; i < count; ++i)
@@ -1006,7 +999,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
ordered_data.members[f.ordered.dest_member].second = std::move(value); ordered_data.members[f.ordered.dest_member].second = std::move(value);
} }
static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} // LCOV_EXCL_LINE
/// store the members of a new object directly in it /// store the members of a new object directly in it
bool start_stored_object(std::size_t len) bool start_stored_object(std::size_t len)
@@ -1310,7 +1303,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
{ {
// a new member is null and holds nothing: construct the value // a new member is null and holds nothing: construct the value
// over it instead of destroying and swapping // over it instead of destroying and swapping
::new (static_cast<void*>(&member)) BasicJsonType(std::forward<Value>(v)); construct_member(member, std::forward<Value>(v), std::is_nothrow_constructible<BasicJsonType, Value> {});
} }
else else
{ {
@@ -1319,6 +1312,26 @@ class json_sax_dom_parser : public json_sax_dom_error_state
} }
} }
/// constructing the value cannot throw (for example a number or a boolean): construct it in place
template<typename Value>
JSON_HEDLEY_ALWAYS_INLINE
static void construct_member(BasicJsonType& member, Value&& v, std::true_type /*nothrow*/)
{
::new (static_cast<void*>(&member)) BasicJsonType(std::forward<Value>(v));
}
/// constructing the value can throw (for example a container or a string), and a
/// constructor that was interrupted must not have touched the member (#5985):
/// create the value first, then move it over the member (the move constructor is
/// noexcept)
template<typename Value>
JSON_HEDLEY_ALWAYS_INLINE
static void construct_member(BasicJsonType& member, Value&& v, std::false_type /*may throw*/)
{
BasicJsonType value(std::forward<Value>(v));
::new (static_cast<void*>(&member)) BasicJsonType(std::move(value));
}
/// a custom base class may care about the lifetime of the value /// a custom base class may care about the lifetime of the value
template<typename Value> template<typename Value>
static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/) static void set_member(BasicJsonType& member, Value&& v, std::false_type /*custom base*/)
@@ -1621,7 +1634,7 @@ class json_sax_dom_binary_parser : public json_sax_dom_error_state
lexer_t* m_lexer_ref = nullptr; lexer_t* m_lexer_ref = nullptr;
}; };
template<typename BasicJsonType, typename InputAdapterType = string_input_adapter_type> template<typename BasicJsonType, typename InputAdapterType>
class json_sax_dom_callback_parser : public json_sax_dom_error_state class json_sax_dom_callback_parser : public json_sax_dom_error_state
{ {
public: public:
@@ -1744,7 +1757,7 @@ class json_sax_dom_callback_parser : public json_sax_dom_error_state
key_stack.push_back(val); key_stack.push_back(val);
// add discarded value at the given key and store the reference for later // add discarded value at the given key and store the reference for later
if (keep && ref_stack.back()) if (keep)
{ {
auto& obj = *ref_stack.back()->m_data.m_value.object; auto& obj = *ref_stack.back()->m_data.m_value.object;
// a single lookup: insert the placeholder, or find the existing member // a single lookup: insert the placeholder, or find the existing member
@@ -1801,7 +1814,7 @@ class json_sax_dom_callback_parser : public json_sax_dom_error_state
const string_t object_key = pop_container(); const string_t object_key = pop_container();
if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_structured()) if (!ref_stack.empty() && ref_stack.back())
{ {
// remove discarded value // remove discarded value
remove_discarded_value(*ref_stack.back(), object_key); remove_discarded_value(*ref_stack.back(), object_key);
@@ -1967,8 +1980,7 @@ class json_sax_dom_callback_parser : public json_sax_dom_error_state
*/ */
string_t current_key() const string_t current_key() const
{ {
if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_object() if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_object())
&& !key_stack.empty())
{ {
return key_stack.back(); return key_stack.back();
} }
+37 -681
View File
@@ -10,7 +10,7 @@
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t #include <cstdint> // uint32_t
#include <cstdio> // snprintf #include <cstdio> // snprintf
#include <cstring> // memcmp #include <cstring> // memcmp
#include <initializer_list> // initializer_list #include <initializer_list> // initializer_list
@@ -300,14 +300,16 @@ class lexer : public lexer_base<BasicJsonType>
/// calls would (including the position and token bookkeeping of a /// calls would (including the position and token bookkeeping of a
/// streaming adapter, see bulk_consume) and leaves @a current holding the last of /// streaming adapter, see bulk_consume) and leaves @a current holding the last of
/// the 4 digits, just as the last such get() would; the codepoint is /// the 4 digits, just as the last such get() would; the codepoint is
/// written to @a out. Makes no state change and returns false for a /// written to @a out. Makes no state change and returns false for fewer
/// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not /// than 4 remaining bytes or any of the 4 bytes not being a hex digit, so
/// being a hex digit, so the caller falls back unchanged to the /// the caller falls back unchanged to the per-character loop, which then
/// per-character loop, which then reports the same diagnostic (stopping /// reports the same diagnostic (stopping at the first invalid digit) as
/// at the first invalid digit) as before this optimization. /// before this optimization.
bool get_codepoint_bulk(std::true_type /*bulk*/, int& out) bool get_codepoint_bulk(std::true_type /*bulk*/, int& out)
{ {
if (next_unget || ia.bulk_remaining() < 4) // no unget is pending inside a string (see get_after_string_run())
JSON_ASSERT(!next_unget);
if (ia.bulk_remaining() < 4)
{ {
return false; return false;
} }
@@ -385,20 +387,6 @@ class lexer : public lexer_base<BasicJsonType>
return codepoint; return codepoint;
} }
/// the code point of the surrogate pair @a high (U+D800..U+DBFF) and @a low (U+DC00..U+DFFF)
static constexpr int combine_surrogates(const int high, const int low) noexcept
{
return static_cast<int>(
// high surrogate occupies the most significant 22 bits
(static_cast<unsigned int>(high) << 10u)
// low surrogate occupies the least significant 15 bits
+ static_cast<unsigned int>(low)
// there is still the 0xD800, 0xDC00, and 0x10000 noise
// in the result, so we have to subtract with:
// (0xD800 << 10) + DC00 - 0x10000 = 0x35FDC00
- 0x35FDC00u);
}
/*! /*!
@brief check if the next byte(s) are inside a given range @brief check if the next byte(s) are inside a given range
@@ -432,7 +420,6 @@ class lexer : public lexer_base<BasicJsonType>
else else
{ {
error_message = "invalid string: ill-formed UTF-8 byte"; error_message = "invalid string: ill-formed UTF-8 byte";
error_cause = error_kind::string_ill_formed_utf8;
return false; return false;
} }
} }
@@ -452,11 +439,10 @@ class lexer : public lexer_base<BasicJsonType>
template<bool Keep> template<bool Keep>
char_int_type get_after_string_run(std::true_type /*bulk*/, std::integral_constant<bool, Keep> keep) char_int_type get_after_string_run(std::true_type /*bulk*/, std::integral_constant<bool, Keep> keep)
{ {
// a pending unget must be consumed through the normal path first // scan_string() is entered from scan_after_whitespace(), whose current
if (next_unget) // was read by skip_whitespace() (its first get() clears a pending
{ // unget), and nothing in scan_string() calls unget()
return get(); JSON_ASSERT(!next_unget);
}
const std::size_t remaining = ia.bulk_remaining(); const std::size_t remaining = ia.bulk_remaining();
if (remaining == 0) if (remaining == 0)
{ {
@@ -556,7 +542,6 @@ class lexer : public lexer_base<BasicJsonType>
case char_traits<char_type>::eof(): case char_traits<char_type>::eof():
{ {
error_message = "invalid string: missing closing quote"; error_message = "invalid string: missing closing quote";
error_cause = error_kind::string;
return token_type::parse_error; return token_type::parse_error;
} }
@@ -613,7 +598,6 @@ class lexer : public lexer_base<BasicJsonType>
if (JSON_HEDLEY_UNLIKELY(codepoint1 == -1)) if (JSON_HEDLEY_UNLIKELY(codepoint1 == -1))
{ {
error_message = "invalid string: '\\u' must be followed by 4 hex digits"; error_message = "invalid string: '\\u' must be followed by 4 hex digits";
error_cause = error_kind::string_invalid_escape;
return token_type::parse_error; return token_type::parse_error;
} }
@@ -621,23 +605,13 @@ class lexer : public lexer_base<BasicJsonType>
if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF) if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF)
{ {
// expect next \uxxxx entry // expect next \uxxxx entry
if (JSON_HEDLEY_LIKELY(get() == '\\')) if (JSON_HEDLEY_LIKELY(get() == '\\' && get() == 'u'))
{ {
if (JSON_HEDLEY_UNLIKELY(get() != 'u'))
{
// current is the character escaped by the backslash
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
error_cause = error_kind::string_invalid_escape;
string_error_resume = resume_kind::escaped_character;
return token_type::parse_error;
}
const int codepoint2 = get_codepoint(); const int codepoint2 = get_codepoint();
if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1)) if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1))
{ {
error_message = "invalid string: '\\u' must be followed by 4 hex digits"; error_message = "invalid string: '\\u' must be followed by 4 hex digits";
error_cause = error_kind::string_invalid_escape;
return token_type::parse_error; return token_type::parse_error;
} }
@@ -645,23 +619,25 @@ class lexer : public lexer_base<BasicJsonType>
if (JSON_HEDLEY_LIKELY(0xDC00 <= codepoint2 && codepoint2 <= 0xDFFF)) if (JSON_HEDLEY_LIKELY(0xDC00 <= codepoint2 && codepoint2 <= 0xDFFF))
{ {
// overwrite codepoint // overwrite codepoint
codepoint = combine_surrogates(codepoint1, codepoint2); codepoint = static_cast<int>(
// high surrogate occupies the most significant 22 bits
(static_cast<unsigned int>(codepoint1) << 10u)
// low surrogate occupies the least significant 15 bits
+ static_cast<unsigned int>(codepoint2)
// there is still the 0xD800, 0xDC00, and 0x10000 noise
// in the result, so we have to subtract with:
// (0xD800 << 10) + DC00 - 0x10000 = 0x35FDC00
- 0x35FDC00u);
} }
else else
{ {
// the second escape was read completely and is a
// code point of its own
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF"; error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
error_cause = error_kind::string_invalid_escape;
string_error_resume = resume_kind::after_escape;
string_error_codepoint = codepoint2;
return token_type::parse_error; return token_type::parse_error;
} }
} }
else else
{ {
error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF"; error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF";
error_cause = error_kind::string_invalid_escape;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -669,10 +645,7 @@ class lexer : public lexer_base<BasicJsonType>
{ {
if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF)) if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF))
{ {
// the escape was read completely
error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF"; error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF";
error_cause = error_kind::string_invalid_escape;
string_error_resume = resume_kind::after_escape;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -689,7 +662,6 @@ class lexer : public lexer_base<BasicJsonType>
// other characters after escape // other characters after escape
default: default:
error_message = "invalid string: forbidden character after backslash"; error_message = "invalid string: forbidden character after backslash";
error_cause = error_kind::string;
return token_type::parse_error; return token_type::parse_error;
} }
@@ -766,7 +738,6 @@ class lexer : public lexer_base<BasicJsonType>
} }
}; };
error_message = control_character_messages[static_cast<std::size_t>(current)]; error_message = control_character_messages[static_cast<std::size_t>(current)];
error_cause = error_kind::string;
return token_type::parse_error; return token_type::parse_error;
} }
@@ -989,7 +960,6 @@ class lexer : public lexer_base<BasicJsonType>
default: default:
{ {
error_message = "invalid string: ill-formed UTF-8 byte"; error_message = "invalid string: ill-formed UTF-8 byte";
error_cause = error_kind::string_ill_formed_utf8;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -1045,7 +1015,6 @@ class lexer : public lexer_base<BasicJsonType>
case char_traits<char_type>::eof(): case char_traits<char_type>::eof():
{ {
error_message = "invalid comment; missing closing '*/'"; error_message = "invalid comment; missing closing '*/'";
error_cause = error_kind::unclosed_comment;
return false; return false;
} }
@@ -1076,7 +1045,6 @@ class lexer : public lexer_base<BasicJsonType>
default: default:
{ {
error_message = "invalid comment; expecting '/' or '*' after '/'"; error_message = "invalid comment; expecting '/' or '*' after '/'";
error_cause = error_kind::other;
return false; return false;
} }
} }
@@ -1202,7 +1170,6 @@ scan_number_minus:
default: default:
{ {
error_message = "invalid number; expected digit after '-'"; error_message = "invalid number; expected digit after '-'";
error_cause = error_kind::number;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -1289,7 +1256,6 @@ scan_number_decimal1:
default: default:
{ {
error_message = "invalid number; expected digit after '.'"; error_message = "invalid number; expected digit after '.'";
error_cause = error_kind::number;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -1358,7 +1324,6 @@ scan_number_exponent:
{ {
error_message = error_message =
"invalid number; expected '+', '-', or digit after exponent"; "invalid number; expected '+', '-', or digit after exponent";
error_cause = error_kind::number;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -1385,7 +1350,6 @@ scan_number_sign:
default: default:
{ {
error_message = "invalid number; expected digit after exponent sign"; error_message = "invalid number; expected digit after exponent sign";
error_cause = error_kind::number;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -1447,7 +1411,7 @@ scan_number_done:
return token_type::value_unsigned; return token_type::value_unsigned;
} }
} }
else if (number_type == token_type::value_integer) else // value_integer (the callers handle value_float themselves)
{ {
if (parse_integer_signed(first, last, value_integer)) if (parse_integer_signed(first, last, value_integer))
{ {
@@ -1553,11 +1517,9 @@ scan_number_done:
*/ */
token_type scan_number_bulk_contiguous() token_type scan_number_bulk_contiguous()
{ {
// a pending unget offsets the buffer position from current; fall back // called right after scan_after_whitespace() read current (see
if (next_unget) // get_after_string_run()), so no unget is pending
{ JSON_ASSERT(!next_unget);
return token_type::uninitialized;
}
const std::size_t rem = ia.bulk_remaining(); const std::size_t rem = ia.bulk_remaining();
if (rem == 0) if (rem == 0)
{ {
@@ -1578,11 +1540,7 @@ scan_number_done:
if (data[0] == '-') if (data[0] == '-')
{ {
number_type = token_type::value_integer; number_type = token_type::value_integer;
i = 1; i = 1; // avail >= 2: rem == 0 returned above
if (i >= avail)
{
return token_type::uninitialized;
}
} }
const std::size_t integer_begin = i; const std::size_t integer_begin = i;
if (data[i] == '0') if (data[i] == '0')
@@ -1813,7 +1771,6 @@ scan_number_done:
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != static_cast<char_type>(rest[i]))) if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != static_cast<char_type>(rest[i])))
{ {
error_message = "invalid literal"; error_message = "invalid literal";
error_cause = error_kind::other;
return token_type::parse_error; return token_type::parse_error;
} }
} }
@@ -1825,7 +1782,10 @@ scan_number_done:
/// the error at the right character /// the error at the right character
JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/) JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/)
{ {
if (next_unget || ia.bulk_remaining() < length // called right after scan_after_whitespace() read current (see
// get_after_string_run()), so no unget is pending
JSON_ASSERT(!next_unget);
if (ia.bulk_remaining() < length
|| std::memcmp(ia.bulk_data(), rest, length) != 0) || std::memcmp(ia.bulk_data(), rest, length) != 0)
{ {
return false; return false;
@@ -2232,25 +2192,17 @@ scan_number_done:
return position.chars_read_total; return position.chars_read_total;
} }
/// seekable adapter: count the lines in the consumed input. Only the /// seekable adapter: count the lines in the consumed input; O(input read
/// input consumed since the previous call is scanned, so a parse that /// so far), but only called on errors
/// reports many errors (a SAX parser recovering from them) stays linear
/// in the input instead of rescanning it for every error.
position_t get_position_impl(std::true_type /*lazy*/) const position_t get_position_impl(std::true_type /*lazy*/) const
{ {
position_t result; position_t result;
result.chars_read_total = get_chars_read_total(); result.chars_read_total = get_chars_read_total();
// reads at EOF count as characters but are not part of the input // reads at EOF count as characters but are not part of the input
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count()); const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
if (JSON_HEDLEY_UNLIKELY(consumed < position_cache.consumed)) std::size_t line_start = 0;
{ result.lines_read = ia.count_consumed_lines(consumed, line_start);
// the consumed input never shrinks; recount from the start if it did result.chars_read_current_line = result.chars_read_total - line_start;
position_cache = {};
}
position_cache.lines += ia.count_consumed_lines(position_cache.consumed, consumed, position_cache.line_start);
position_cache.consumed = consumed;
result.lines_read = position_cache.lines;
result.chars_read_current_line = result.chars_read_total - position_cache.line_start;
return result; return result;
} }
@@ -2434,7 +2386,6 @@ scan_number_done:
if (get_chars_read_total() == 0 && !skip_bom()) if (get_chars_read_total() == 0 && !skip_bom())
{ {
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
error_cause = error_kind::other;
return token_type::parse_error; return token_type::parse_error;
} }
@@ -2544,583 +2495,10 @@ scan_number_done:
// error // error
default: default:
error_message = "invalid literal"; error_message = "invalid literal";
error_cause = error_kind::other;
return token_type::parse_error; return token_type::parse_error;
} }
} }
public:
/////////////////////
// error recovery
/////////////////////
/*!
@brief make the best of the token that scan() rejected
Called by the parser after scan() returned token_type::parse_error and the
SAX parser asked to recover from the error (see #3989). Keeps what can be
read of the token and skips the rest:
- A string keeps its characters. An unknown escape stands for the escaped
character itself (as in JavaScript), an invalid Unicode escape and ill-formed
UTF-8 become U+FFFD, and a control character is kept. A line break or the
end of the input ends a string that lacks its closing quote.
- A number keeps its longest valid prefix, e.g. `1` for `1.` or `1e+`.
- A block comment that is not closed runs to the end of the input.
- Anything else is skipped.
The rest of an invalid token is skipped up to the next delimiter
(whitespace, a structural character, or a quote). A delimiter that the
invalid token consumed is returned to the input, so that the next scan()
reads it.
@return token_type::value_string or a number token type if a string or a
number could be read, token_type::end_of_input for a block comment
that is not closed, token_type::uninitialized otherwise
*/
token_type recover_token()
{
const resume_kind resume = string_error_resume;
const int codepoint = string_error_codepoint;
string_error_resume = resume_kind::character;
string_error_codepoint = -1;
const error_kind cause = error_cause;
error_cause = error_kind::other;
switch (cause)
{
case error_kind::string:
case error_kind::string_invalid_escape:
case error_kind::string_ill_formed_utf8:
return recover_string(cause, resume, codepoint);
case error_kind::number:
return recover_number();
case error_kind::unclosed_comment:
// the comment runs to the end of the input
return token_type::end_of_input;
case error_kind::other:
default:
skip_to_delimiter();
return token_type::uninitialized;
}
}
/*!
@brief return the token that scan() read last to the input, so that the
next scan() reads it again
Called by the parser when recovering from an error. The token must be a
single character (',', ':', '[', ']', '{', or '}') or the end of the
input, and scan() must have read it last.
*/
void unget_token()
{
JSON_ASSERT(!next_unget);
unget();
}
/*!
@brief let the token string for the next error begin at the current character
The token string of an error reaches back to the beginning of the last
string or number. After an error, the parser calls this function so that
the next error does not report (and, with many errors, copy) everything
read since then.
*/
void restart_token_string()
{
restart_token_string_impl(std::integral_constant<bool, lazy_token_string> {});
}
private:
/// how recover_string() continues after the error scan_string() reported
enum class resume_kind : std::uint8_t
{
/// current is the next character of the string (or the end of input)
character,
/// current is the character escaped by the preceding backslash
escaped_character,
/// current is the last character of a complete escape
after_escape
};
/// what kind of token scan() rejected, which decides how recover_token() repairs it
enum class error_kind : std::uint8_t
{
/// anything not listed below; skipped up to the next delimiter
other,
/// a string
string,
/// a string with an invalid Unicode escape, which becomes U+FFFD
string_invalid_escape,
/// a string with ill-formed UTF-8, which becomes U+FFFD
string_ill_formed_utf8,
/// a number
number,
/// a block comment that is not closed
unclosed_comment
};
/// whether current ends an invalid token (see recover_token())
bool current_is_delimiter() const noexcept
{
switch (current)
{
case ' ':
case '\t':
case '\n':
case '\r':
case '[':
case ']':
case '{':
case '}':
case ',':
case ':':
case '\"':
#if !JSON_STRICT_NUL_HANDLING
case '\0':
#endif
case char_traits<char_type>::eof():
return true;
case '/':
return ignore_comments;
default:
return false;
}
}
/// skip the rest of an invalid token and return its delimiter to the input
void skip_to_delimiter()
{
while (!current_is_delimiter())
{
get();
}
if (current != char_traits<char_type>::eof())
{
unget();
}
}
/// append U+FFFD REPLACEMENT CHARACTER to token_buffer
void add_replacement_character()
{
add_codepoint(0xFFFD);
}
/// append the UTF-8 encoding of @a codepoint (not a surrogate) to token_buffer
void add_codepoint(const int codepoint)
{
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
{
add(static_cast<char_int_type>(byte));
});
}
/// append a code point read from a Unicode escape; a surrogate becomes U+FFFD
void add_escaped_codepoint(const int codepoint)
{
if (0xD800 <= codepoint && codepoint <= 0xDFFF)
{
add_replacement_character();
}
else
{
add_codepoint(codepoint);
}
}
/*!
@brief remove an incomplete UTF-8 sequence from the end of token_buffer
next_byte_in_range() adds the bytes of a sequence as it checks them, so
when it rejects a byte, the beginning of the sequence is already in
token_buffer, which otherwise holds only complete sequences.
@return whether an incomplete sequence was removed
*/
bool remove_incomplete_utf8_sequence()
{
std::size_t lead = token_buffer.size();
std::size_t continuation_bytes = 0;
while (lead > 0 && continuation_bytes < 3
&& (static_cast<unsigned char>(token_buffer[lead - 1]) & 0xC0u) == 0x80u)
{
--lead;
++continuation_bytes;
}
if (lead == 0)
{
return false;
}
const auto lead_byte = static_cast<unsigned char>(token_buffer[lead - 1]);
std::size_t expected = 0;
if (lead_byte >= 0xF0)
{
expected = 3;
}
else if (lead_byte >= 0xE0)
{
expected = 2;
}
else if (lead_byte >= 0xC0)
{
expected = 1;
}
if (continuation_bytes >= expected)
{
return false;
}
token_buffer.resize(lead - 1);
return true;
}
/*!
@brief read the UTF-8 sequence that begins with current, which is not ASCII
@return whether the next character must be read; false if current still
needs to be handled, because it does not belong to the sequence
*/
bool recover_utf8_sequence()
{
// the number of continuation bytes and the range of the first one;
// see the ranges in scan_string()
std::size_t count = 0;
char_int_type low = 0x80;
char_int_type high = 0xBF;
if (current >= 0xC2 && current <= 0xDF)
{
count = 1;
}
else if (current >= 0xE0 && current <= 0xEF)
{
count = 2;
low = (current == 0xE0) ? 0xA0 : 0x80;
high = (current == 0xED) ? 0x9F : 0xBF;
}
else if (current >= 0xF0 && current <= 0xF4)
{
count = 3;
low = (current == 0xF0) ? 0x90 : 0x80;
high = (current == 0xF4) ? 0x8F : 0xBF;
}
else
{
// an ill-formed byte
add_replacement_character();
return true;
}
const std::size_t start = token_buffer.size();
add(current);
for (std::size_t i = 0; i < count; ++i)
{
get();
if (current < low || current > high)
{
token_buffer.resize(start);
add_replacement_character();
return false;
}
add(current);
low = 0x80;
high = 0xBF;
}
return true;
}
/*!
@brief read the low surrogate that must follow the high surrogate @a high
@return whether the next character must be read; false if current still
needs to be handled
*/
bool recover_low_surrogate(int high)
{
while (true)
{
if (get() != '\\')
{
add_replacement_character();
return false;
}
if (get() != 'u')
{
add_replacement_character();
// not 'u', so this does not come back here
return recover_escape();
}
const int low = get_codepoint();
if (low == -1)
{
add_replacement_character();
return false;
}
if (0xDC00 <= low && low <= 0xDFFF)
{
add_codepoint(combine_surrogates(high, low));
return true;
}
// high has no low surrogate
add_replacement_character();
if (low < 0xD800 || low > 0xDBFF)
{
add_codepoint(low);
return true;
}
// another high surrogate
high = low;
}
}
/*!
@brief read the escape whose backslash was read; current is the escaped character
@return whether the next character must be read; false if current still
needs to be handled
*/
bool recover_escape()
{
switch (current)
{
case '\"':
add('\"');
return true;
case '\\':
add('\\');
return true;
case '/':
add('/');
return true;
case 'b':
add('\b');
return true;
case 'f':
add('\f');
return true;
case 'n':
add('\n');
return true;
case 'r':
add('\r');
return true;
case 't':
add('\t');
return true;
case 'u':
{
const int codepoint = get_codepoint();
if (codepoint == -1)
{
add_replacement_character();
return false;
}
if (0xD800 <= codepoint && codepoint <= 0xDBFF)
{
return recover_low_surrogate(codepoint);
}
add_escaped_codepoint(codepoint);
return true;
}
// an unknown escape stands for the escaped character
default:
return false;
}
}
/*!
@brief read the rest of a string after scan_string() rejected it
token_buffer holds what scan_string() read before the error. See
recover_token() for how errors are repaired.
@param[in] cause the kind of error scan_string() reported
@param[in] resume how to continue, see resume_kind
@param[in] codepoint for a high surrogate followed by an escape of another
code point: that code point; -1 otherwise
*/
token_type recover_string(const error_kind cause, const resume_kind resume, const int codepoint)
{
// whether the next character must be read before it can be handled
bool fetch = false;
if (cause == error_kind::string_invalid_escape
|| (cause == error_kind::string_ill_formed_utf8 && remove_incomplete_utf8_sequence()))
{
add_replacement_character();
}
switch (resume)
{
case resume_kind::escaped_character:
fetch = recover_escape();
break;
case resume_kind::after_escape:
if (0xD800 <= codepoint && codepoint <= 0xDBFF)
{
fetch = recover_low_surrogate(codepoint);
}
else
{
if (codepoint != -1)
{
add_escaped_codepoint(codepoint);
}
fetch = true;
}
break;
case resume_kind::character:
default:
break;
}
while (true)
{
if (fetch)
{
get();
}
fetch = true;
switch (current)
{
case '\"':
// a line break or the end of the input ends a string that
// lacks its closing quote
case '\n':
case '\r':
case char_traits<char_type>::eof():
return token_type::value_string;
#if !JSON_STRICT_NUL_HANDLING
case '\0':
// the end of the input, see scan()
unget();
return token_type::value_string;
#endif
case '\\':
get();
fetch = recover_escape();
break;
default:
if (current < 0x80)
{
// including control characters
add(current);
}
else
{
fetch = recover_utf8_sequence();
}
break;
}
}
}
/*!
@brief keep the longest valid prefix of a number that scan_number() rejected
token_buffer holds the characters scan_number() accepted before the error,
so the prefix ends at its last digit.
*/
token_type recover_number()
{
// only size(), operator[], and resize() are used, which every string
// type the library supports provides
std::size_t length = token_buffer.size();
while (length != 0 && (token_buffer[length - 1] < '0' || token_buffer[length - 1] > '9'))
{
--length;
}
token_buffer.resize(length);
if (length == 0)
{
skip_to_delimiter();
return token_type::uninitialized;
}
if (decimal_point_position >= length)
{
decimal_point_position = std::string::npos;
}
std::size_t exponent = std::string::npos;
for (std::size_t i = 0; i < length; ++i)
{
if (token_buffer[i] == 'e' || token_buffer[i] == 'E')
{
exponent = i;
break;
}
}
const std::size_t mantissa_end = (exponent == std::string::npos) ? length : exponent;
token_type number_type = token_type::value_unsigned;
if (decimal_point_position != std::string::npos || exponent != std::string::npos)
{
number_type = token_type::value_float;
}
else if (token_buffer[0] == '-')
{
number_type = token_type::value_integer;
}
const token_type result = convert_number(number_type, mantissa_end);
skip_to_delimiter();
return result;
}
/// seekable or token-tracking adapter: the token string begins at current
void restart_token_string_impl(std::true_type /*lazy*/)
{
restart_token_string_lazy(std::integral_constant<bool, adapter_token> {});
}
/// seekable adapter: the token string begins at current, which was consumed
void restart_token_string_lazy(std::false_type /*adapter_token*/) noexcept
{
const std::size_t consumed = ia.get_consumed_count();
token_string_start = (consumed > 0 && current != char_traits<char_type>::eof()) ? consumed - 1 : consumed;
}
/// token-tracking adapter: the token begins with current, or with the next
/// character if current was put back or is the end of input
void restart_token_string_lazy(std::true_type /*adapter_token*/)
{
if (!next_unget && current != char_traits<char_type>::eof())
{
ia.mark_token_start(current);
}
else
{
ia.begin_token();
}
}
/// streaming adapter: the token string begins at current; a character
/// that was put back is copied again when it is read again
void restart_token_string_impl(std::false_type /*lazy*/)
{
token_string.clear();
if (!next_unget && current != char_traits<char_type>::eof())
{
token_string.push_back(char_traits<char_type>::to_char_type(current));
}
}
/// input adapter /// input adapter
InputAdapterType ia; InputAdapterType ia;
@@ -3145,19 +2523,6 @@ scan_number_done:
/// that unget() can restore the column instead of leaving it at 0 /// that unget() can restore the column instead of leaving it at 0
std::size_t chars_read_before_newline = 0; std::size_t chars_read_before_newline = 0;
/// seekable adapter: what get_position() counted last, so that the next
/// call only scans the input consumed since
struct position_cache_t
{
/// consumed characters scanned so far
std::size_t consumed = 0;
/// newlines among them
std::size_t lines = 0;
/// index after the last of these newlines
std::size_t line_start = 0;
};
mutable position_cache_t position_cache {};
/// raw input token string for error messages; only populated for streaming /// raw input token string for error messages; only populated for streaming
/// adapters (seekable adapters reconstruct it lazily via token_string_start) /// adapters (seekable adapters reconstruct it lazily via token_string_start)
std::vector<char_type> token_string {}; std::vector<char_type> token_string {};
@@ -3178,15 +2543,6 @@ scan_number_done:
/// a description of occurred lexer errors /// a description of occurred lexer errors
const char* error_message = ""; const char* error_message = "";
/// what kind of token scan() rejected; set together with error_message
error_kind error_cause = error_kind::other;
/// how recover_token() continues a string that scan_string() rejected;
/// set only on the error paths that need more than error_message
resume_kind string_error_resume = resume_kind::character;
/// the code point of the second escape when a high surrogate is followed
/// by an escape that is not a low surrogate; -1 otherwise
int string_error_codepoint = -1;
// number values // number values
number_integer_t value_integer = 0; number_integer_t value_integer = 0;
number_unsigned_t value_unsigned = 0; number_unsigned_t value_unsigned = 0;
+27 -744
View File
@@ -152,72 +152,25 @@ class parser
bool accept(const bool strict = true) bool accept(const bool strict = true)
{ {
json_sax_acceptor<BasicJsonType> sax_acceptor; json_sax_acceptor<BasicJsonType> sax_acceptor;
return sax_parse_impl<false>(&sax_acceptor, strict); return sax_parse(&sax_acceptor, strict);
} }
/*!
@brief public SAX interface
If the SAX parser's parse_error() returns true, the parser recovers from
the error: it repairs the input and continues (see #3989).
@param[in] sax the SAX parser
@param[in] strict whether to expect the last token to be EOF
@return whether the input was parsed without errors and no SAX event
returned false
*/
template<typename SAX> template<typename SAX>
JSON_HEDLEY_NON_NULL(2) JSON_HEDLEY_NON_NULL(2)
bool sax_parse(SAX* sax, const bool strict = true) bool sax_parse(SAX* sax, const bool strict = true)
{
return sax_parse_impl<true>(sax, strict);
}
private:
/// what sax_parse_internal() does after an object key was expected
enum class next_step : std::uint8_t
{
/// stop parsing
stop,
/// parse a value that begins with last_token
parse_value,
/// evaluate the state of the innermost container, which reads
/// last_token again
evaluate_state
};
template<bool AllowRecovery, typename SAX>
JSON_HEDLEY_NON_NULL(2)
bool sax_parse_impl(SAX* sax, const bool strict)
{ {
(void)detail::is_sax_static_asserts<SAX, BasicJsonType> {}; (void)detail::is_sax_static_asserts<SAX, BasicJsonType> {};
const bool result = sax_parse_internal<AllowRecovery>(sax); const bool result = sax_parse_internal(sax);
if (result) if (result)
{ {
if (strict) return finish_input(*sax, strict);
{ }
// strict mode: next byte must be EOF; after recovering from an
// error, the end of the input may already have been read
if (last_token != token_type::end_of_input && get_token() != token_type::end_of_input)
{
// the value is complete, so there is nothing to recover
static_cast<void>(report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::end_of_input, "value"), nullptr),
std::integral_constant<bool, AllowRecovery> {}));
return false; return false;
} }
}
else
{
// the caller keeps using the input: position it right after
// the value by leaving the character that terminated it
m_lexer.release_lookahead();
}
}
return result && !error_reported;
}
private:
/// the text of the current float token for @a sax (see sax_ignores_number_text) /// the text of the current float token for @a sax (see sax_ignores_number_text)
template<typename SAX> template<typename SAX>
const string_t& number_float_text(SAX* /*sax*/) const string_t& number_float_text(SAX* /*sax*/)
@@ -285,26 +238,17 @@ class parser
template<typename DomSax> template<typename DomSax>
bool parse_dom(DomSax& sdp, const bool strict) bool parse_dom(DomSax& sdp, const bool strict)
{ {
sax_parse_internal<false>(&sdp); sax_parse_internal(&sdp);
static_cast<void>(finish_input(sdp, strict)); static_cast<void>(finish_input(sdp, strict));
return !sdp.is_errored(); return !sdp.is_errored();
} }
/*! template<typename SAX>
@brief parse a JSON value and pass it to a SAX parser
@tparam AllowRecovery whether to recover from an error if the SAX parser's
parse_error() returns true; false for the SAX parsers
of parse() and accept(), which never do, so that no
code for recovering is generated for them
*/
template<bool AllowRecovery, typename SAX>
JSON_HEDLEY_NON_NULL(2) JSON_HEDLEY_NON_NULL(2)
bool sax_parse_internal(SAX* sax) bool sax_parse_internal(SAX* sax)
{ {
const std::integral_constant<bool, AllowRecovery> allow_recovery{};
// value to avoid a goto (see comment where set to true) // value to avoid a goto (see comment where set to true)
bool skip_to_state_evaluation = false; bool skip_to_state_evaluation = false;
@@ -343,18 +287,10 @@ class parser
break; break;
} }
// remember we are now inside an object // parse key
states.push_back(0);
// parse key (the steps of parse_key(), which are
// repeated here and below for speed)
if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
{ {
if (!continue_after(key_error(sax, allow_recovery, false), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
{
return false;
}
continue;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token()))) if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
{ {
@@ -364,13 +300,12 @@ class parser
// parse separator (:) // parse separator (:)
if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator)))
{ {
if (!continue_after(key_error(sax, allow_recovery, true), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::name_separator, "object separator"));
{
return false;
}
continue;
} }
// remember we are now inside an object
states.push_back(0);
// parse values // parse values
get_token(); get_token();
continue; continue;
@@ -406,11 +341,9 @@ class parser
if (JSON_HEDLEY_UNLIKELY(!std::isfinite(res))) if (JSON_HEDLEY_UNLIKELY(!std::isfinite(res)))
{ {
if (!overflow_error(sax, res, allow_recovery)) return sax->parse_error(m_lexer.get_position(),
{ m_lexer.get_token_string(),
return false; out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr));
}
break;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax)))) if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax))))
@@ -478,63 +411,16 @@ class parser
case token_type::parse_error: case token_type::parse_error:
{ {
// using "uninitialized" to avoid an "expected" message // using "uninitialized" to avoid an "expected" message
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::uninitialized, "value"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::uninitialized, "value"));
{
return false;
}
// recover: keep what can be read of the token
recover_token(allow_recovery);
if (last_token != token_type::uninitialized)
{
// a string or a number
continue;
}
if (states.empty())
{
// look for the value after the garbage
if (!skip_to_value(allow_recovery))
{
return false;
}
continue;
}
// nothing could be read
if (JSON_HEDLEY_UNLIKELY(!sax->null()))
{
return false;
}
break;
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
// there is nothing to recover return syntax_error(*sax, empty_input_message());
static_cast<void>(report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(),
empty_input_message(), nullptr), allow_recovery));
return false;
} }
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::literal_or_value, "value"));
{
return false;
}
// recover: the input ends where a value is missing
if (states.empty())
{
// there is no value
return false;
}
if (!recover_missing_value(sax, states, allow_recovery))
{
return false;
}
// the state evaluation reads the token again
unget_token(allow_recovery);
skip_to_state_evaluation = true;
continue;
} }
case token_type::uninitialized: case token_type::uninitialized:
case token_type::end_array: case token_type::end_array:
@@ -544,35 +430,7 @@ class parser
case token_type::literal_or_value: case token_type::literal_or_value:
default: // the last token was unexpected default: // the last token was unexpected
{ {
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::literal_or_value, "value"));
{
return false;
}
// recover
if (states.empty())
{
// look for the value after the garbage
if (!skip_to_value(allow_recovery))
{
return false;
}
continue;
}
if (last_token == token_type::name_separator)
{
// a stray ':'; the value may follow
get_token();
continue;
}
if (!recover_missing_value(sax, states, allow_recovery))
{
return false;
}
// the state evaluation reads the token again
unget_token(allow_recovery);
skip_to_state_evaluation = true;
continue;
} }
} }
} }
@@ -623,30 +481,7 @@ class parser
continue; continue;
} }
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::end_array, "array"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::end_array, "array"));
{
return false;
}
// recover
if (last_token == token_type::end_of_input)
{
// the input ends inside the array
return close_containers(sax, states, allow_recovery);
}
if (last_token == token_type::end_object)
{
// a wrong closing bracket closes the innermost container
if (JSON_HEDLEY_UNLIKELY(!sax->end_array()))
{
return false;
}
states.pop_back();
skip_to_state_evaluation = true;
}
// otherwise, a missing ',' (or a stray ':', which value
// parsing drops): the next value begins here
continue;
} }
// states.back() is false -> object // states.back() is false -> object
@@ -663,12 +498,9 @@ class parser
// parse key // parse key
if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
{ {
if (!continue_after(key_error(sax, allow_recovery, false), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
{
return false;
}
continue;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token()))) if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
{ {
return false; return false;
@@ -677,11 +509,7 @@ class parser
// parse separator (:) // parse separator (:)
if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator))) if (JSON_HEDLEY_UNLIKELY(!get_token_expecting(token_type::name_separator)))
{ {
if (!continue_after(key_error(sax, allow_recovery, true), skip_to_state_evaluation)) return syntax_error(*sax, exception_message(token_type::name_separator, "object separator"));
{
return false;
}
continue;
} }
// parse values // parse values
@@ -709,544 +537,8 @@ class parser
continue; continue;
} }
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::end_object, "object"), nullptr), allow_recovery)) return syntax_error(*sax, exception_message(token_type::end_object, "object"));
{
return false;
} }
// recover
if (last_token == token_type::end_of_input)
{
// the input ends inside the object
return close_containers(sax, states, allow_recovery);
}
if (last_token == token_type::end_array)
{
// a wrong closing bracket closes the innermost container
if (JSON_HEDLEY_UNLIKELY(!sax->end_object()))
{
return false;
}
states.pop_back();
skip_to_state_evaluation = true;
continue;
}
if (!continue_after(recover_member(sax, allow_recovery), skip_to_state_evaluation))
{
return false;
}
}
}
/*!
@brief continue sax_parse_internal() after a recovery
@return whether to continue parsing
*/
bool continue_after(const next_step step, bool& skip_to_state_evaluation)
{
if (step == next_step::evaluate_state)
{
// the state evaluation reads the token again
m_lexer.unget_token();
skip_to_state_evaluation = true;
}
return step != next_step::stop;
}
/// the parser for parse() and accept() never recovers: stop parsing
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type continue_after(std::false_type /*step*/, bool& /*skip_to_state_evaluation*/) noexcept
{
return {};
}
/*!
@brief parse an object key and the name separator (:) after it
last_token is the token where the key is expected. sax_parse_internal()
repeats these steps rather than calling this function, which is used
when recovering from an error.
@return next_step::parse_value if the value follows, with last_token its
first token; next_step::evaluate_state if the object's state is
to be evaluated after recovering from an error; next_step::stop
to stop parsing
*/
template<typename SAX>
next_step parse_key(SAX* sax)
{
const std::true_type allow_recovery{};
if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string))
{
return key_error(sax, allow_recovery, false);
}
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
{
return next_step::stop;
}
// parse separator (:)
if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator))
{
return key_error(sax, allow_recovery, true);
}
// the value begins with the next token
get_token();
return next_step::parse_value;
}
/*!
@brief report a number that is too large for number_float_t, and recover
from the error by passing the value on; the SAX parser gets the
number's text as well
This is a separate function, as reading other numbers is measurably
slower if the error is handled where they are read.
@param[in] sax the SAX parser
@param[in] value the value that is not finite
@return whether to continue parsing
*/
template<typename SAX>
bool overflow_error(SAX* sax, const number_float_t value, std::true_type allow_recovery)
{
if (!report_error(sax, out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr), allow_recovery))
{
return false;
}
return sax->number_float(value, number_float_text(sax));
}
/// @copydoc overflow_error
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type overflow_error(SAX* sax, const number_float_t /*value*/, std::false_type allow_recovery)
{
return report_error(sax, out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr), allow_recovery);
}
/*!
@brief report a missing key, or a missing name separator (:) after the
key; the parser for parse() and accept() never recovers
@param[in] key_read whether the key was read, so that the name separator
is missing
@return std::false_type, see report_error()
*/
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type key_error(SAX* sax, std::false_type allow_recovery, const bool key_read)
{
return report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), key_read
? exception_message(token_type::name_separator, "object separator")
: exception_message(token_type::value_string, "object key"), nullptr), allow_recovery);
}
/*!
@brief report a missing key, or a missing name separator (:) after the
key, and recover from it
@param[in] key_read whether the key was read, so that the name separator
is missing
*/
template<typename SAX>
next_step key_error(SAX* sax, std::true_type allow_recovery, const bool key_read)
{
if (!key_read)
{
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), nullptr), allow_recovery))
{
return next_step::stop;
}
return recover_key(sax);
}
if (!report_error(sax, parse_error::create(exception_id::syntax_error, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), nullptr), allow_recovery))
{
return next_step::stop;
}
return recover_name_separator(sax);
}
/////////////////////
// error recovery
/////////////////////
/*
The functions below repair an error after the SAX parser's parse_error()
returned true (see #3989). Each mistake is repaired by the smallest local
edit: a missing ',' or ':' is inserted, a stray token is removed, what can
be read of an invalid string or number is kept (see
lexer::recover_token()), a missing value becomes null, a wrong closing
bracket closes the innermost container, and the end of the input closes
all of them. The events stay balanced, and every key() is followed by
exactly one value.
A repair hands a token to the state evaluation, by returning it to the
lexer (lexer::unget_token()) so that the state evaluation reads it again,
only if it is ',', ']', '}', or the end of the input. The state evaluation
hands a token to value or key parsing only if it is none of them, so a
token is never handed back and forth. Every other step reads a token or
closes a container, so parsing always ends.
*/
/*!
@brief report an error to the SAX parser; the parser for parse() and
accept() never recovers
@return std::false_type rather than false: its value is known where the
function is called even if the call is not inlined, so the code
for recovering is not generated
*/
template<typename SAX, typename Exception>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type report_error(SAX* sax, const Exception& ex, std::false_type /*allow_recovery*/)
{
static_cast<void>(sax); // MSVC 2015 does not count calling a static parse_error() as using it
error_reported = true;
static_cast<void>(sax->parse_error(m_lexer.get_position(), m_lexer.get_token_string(), ex));
return {};
}
/*!
@brief report an error to the SAX parser
@return whether to recover from the error
*/
template<typename SAX, typename Exception>
bool report_error(SAX* sax, const Exception& ex, std::true_type /*allow_recovery*/)
{
static_cast<void>(sax); // MSVC 2015 does not count calling a static parse_error() as using it
const std::size_t position = m_lexer.get_position().chars_read_total;
if (error_reported && position == last_error_position && last_token == last_error_token)
{
// a repair handed on the token of the error it repaired; the
// token was reported already, and the SAX parser asked to recover
return true;
}
error_reported = true;
last_error_position = position;
last_error_token = last_token;
if (!sax->parse_error(m_lexer.get_position(), m_lexer.get_token_string(), ex))
{
return false;
}
// the token string of the next error begins here
m_lexer.restart_token_string();
return true;
}
/*!
@brief keep what can be read of the token that the lexer rejected
The error was reported for the rejected token, so it is not reported again
for the token it is repaired to (see lexer::recover_token()).
*/
token_type recover_token()
{
last_token = m_lexer.recover_token();
last_error_position = m_lexer.get_position().chars_read_total;
last_error_token = last_token;
return last_token;
}
// The functions below are called from sax_parse_internal() after an error
// was reported. The parser for parse() and accept() stopped there, so for
// it they are stubs: the code for recovering is then not even referenced,
// and unoptimized builds do not emit it either.
/// @copydoc recover_token()
JSON_INTERNAL_ALWAYS_INLINE
token_type recover_token(std::true_type /*allow_recovery*/)
{
return recover_token();
}
/// @copydoc recover_token()
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type recover_token(std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/// return the token that was read last to the lexer (see lexer::unget_token())
JSON_INTERNAL_ALWAYS_INLINE
void unget_token(std::true_type /*allow_recovery*/)
{
m_lexer.unget_token();
}
/// @copydoc unget_token(std::true_type)
JSON_INTERNAL_ALWAYS_INLINE
static void unget_token(std::false_type /*allow_recovery*/) noexcept {}
/// @copydoc skip_to_value
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type skip_to_value(std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/// @copydoc recover_missing_value
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type recover_missing_value(SAX* /*sax*/, const std::vector<std::uint8_t>& /*states*/, std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/// pass the end events of all open containers
template<typename SAX>
bool close_containers(SAX* sax, std::vector<std::uint8_t>& states, std::true_type /*allow_recovery*/)
{
while (!states.empty())
{
const bool is_array = states.back() != 0;
states.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_array ? !sax->end_array() : !sax->end_object()))
{
return false;
}
}
return true;
}
/// @copydoc close_containers
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
static std::false_type close_containers(SAX* /*sax*/, std::vector<std::uint8_t>& /*states*/, std::false_type /*allow_recovery*/) noexcept
{
return {};
}
/*!
@brief read tokens until one begins a value, skipping everything before
the top-level value
@return whether a value begins with last_token
*/
bool skip_to_value(std::true_type /*allow_recovery*/)
{
while (true)
{
switch (get_token())
{
case token_type::begin_array:
case token_type::begin_object:
case token_type::literal_false:
case token_type::literal_null:
case token_type::literal_true:
case token_type::value_float:
case token_type::value_integer:
case token_type::value_string:
case token_type::value_unsigned:
return true;
case token_type::end_of_input:
return false;
case token_type::parse_error:
recover_token();
if (last_token != token_type::uninitialized)
{
return true;
}
break;
case token_type::uninitialized:
case token_type::end_array:
case token_type::end_object:
case token_type::name_separator:
case token_type::value_separator:
case token_type::literal_or_value:
default:
break;
}
}
}
/*!
@brief skip the rest of an object member that cannot be read
Reads tokens, beginning with last_token, until a ',', '}', or ']' that is
not inside a container that begins in the skipped tokens, or the end of
the input.
*/
void skip_member()
{
std::size_t depth = 0;
while (true)
{
switch (last_token)
{
case token_type::begin_array:
case token_type::begin_object:
++depth;
break;
case token_type::end_array:
case token_type::end_object:
if (depth == 0)
{
return;
}
--depth;
break;
case token_type::value_separator:
if (depth == 0)
{
return;
}
break;
case token_type::end_of_input:
return;
case token_type::parse_error:
recover_token();
break;
case token_type::uninitialized:
case token_type::literal_true:
case token_type::literal_false:
case token_type::literal_null:
case token_type::value_string:
case token_type::value_unsigned:
case token_type::value_integer:
case token_type::value_float:
case token_type::name_separator:
case token_type::literal_or_value:
default:
break;
}
get_token();
}
}
/*!
@brief pass a value where it is missing
last_token is ',', ']', '}', or the end of the input, where a value was
expected. In an object, the key gets null; in an array, a ',' where a
value is missing stands for null (as in JavaScript), while an array that
ends there just ends.
*/
template<typename SAX>
bool recover_missing_value(SAX* sax, const std::vector<std::uint8_t>& states, std::true_type /*allow_recovery*/)
{
JSON_ASSERT(!states.empty());
if (states.back() == 0 || last_token == token_type::value_separator)
{
return sax->null();
}
return true;
}
/// recover from a missing key; last_token is where it was expected
template<typename SAX>
next_step recover_key(SAX* sax)
{
switch (last_token)
{
case token_type::value_separator:
case token_type::end_object:
case token_type::end_array:
case token_type::end_of_input:
// no member: the object's state handles the token
return next_step::evaluate_state;
case token_type::parse_error:
recover_token();
if (last_token == token_type::value_string)
{
// a key that could be repaired
return parse_key(sax);
}
skip_member();
return next_step::evaluate_state;
case token_type::uninitialized:
case token_type::literal_true:
case token_type::literal_false:
case token_type::literal_null:
case token_type::value_string:
case token_type::value_unsigned:
case token_type::value_integer:
case token_type::value_float:
case token_type::begin_array:
case token_type::begin_object:
case token_type::name_separator:
case token_type::literal_or_value:
default:
// a member without a key
skip_member();
return next_step::evaluate_state;
}
}
/// recover from a missing name separator (:) after the key; last_token
/// is where it was expected
template<typename SAX>
next_step recover_name_separator(SAX* sax)
{
switch (last_token)
{
case token_type::value_separator:
case token_type::end_object:
case token_type::end_array:
case token_type::end_of_input:
// the value is missing as well
return sax->null() ? next_step::evaluate_state : next_step::stop;
case token_type::uninitialized:
case token_type::literal_true:
case token_type::literal_false:
case token_type::literal_null:
case token_type::value_string:
case token_type::value_unsigned:
case token_type::value_integer:
case token_type::value_float:
case token_type::begin_array:
case token_type::begin_object:
case token_type::name_separator:
case token_type::parse_error:
case token_type::literal_or_value:
default:
// a missing ':'; the value begins here
return next_step::parse_value;
}
}
/// recover from a token after an object member that is neither ',' nor
/// '}' (nor ']' or the end of the input, which the caller handles)
template<typename SAX>
next_step recover_member(SAX* sax, std::true_type /*allow_recovery*/)
{
if (last_token == token_type::parse_error)
{
recover_token();
}
if (last_token == token_type::value_string)
{
// a missing ','; the next key begins here
return parse_key(sax);
}
skip_member();
return next_step::evaluate_state;
}
/// the parser for parse() and accept() never recovers (and does not come
/// here, as report_error() returned false)
template<typename SAX>
JSON_INTERNAL_ALWAYS_INLINE
std::false_type recover_member(SAX* /*sax*/, std::false_type /*allow_recovery*/) const noexcept
{
return {};
} }
/// get next token from lexer /// get next token from lexer
@@ -1283,10 +575,7 @@ class parser
{ {
std::string error_msg = "syntax error "; std::string error_msg = "syntax error ";
if (!context.empty())
{
error_msg += concat("while parsing ", context, ' '); error_msg += concat("while parsing ", context, ' ');
}
error_msg += "- "; error_msg += "- ";
@@ -1306,7 +595,7 @@ class parser
} }
return error_msg; return error_msg;
} } // LCOV_EXCL_LINE (exception cleanup only)
private: private:
/// callback function /// callback function
@@ -1319,12 +608,6 @@ class parser
const bool allow_exceptions = true; const bool allow_exceptions = true;
/// whether trailing commas in objects and arrays should be ignored (true) or signaled as errors (false) /// whether trailing commas in objects and arrays should be ignored (true) or signaled as errors (false)
const bool ignore_trailing_commas = false; const bool ignore_trailing_commas = false;
/// whether an error was reported to the SAX parser
bool error_reported = false;
/// the position of the last reported error
std::size_t last_error_position = 0;
/// the token of the last reported error
token_type last_error_token = token_type::uninitialized;
}; };
} // namespace detail } // namespace detail
@@ -108,6 +108,9 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
// m_it is already value-initialized, so this is not needed for
// correctness; it is kept because GCC generates faster loops with it
// (iterating a parsed document is 0.5-2.7 % slower without it, #6005)
if (m_object->is_object()) if (m_object->is_object())
{ {
m_it.object_iterator = typename object_t::iterator(); m_it.object_iterator = typename object_t::iterator();
+4 -4
View File
@@ -304,7 +304,7 @@ class json_pointer
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end()); result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end());
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer /// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
@@ -315,7 +315,7 @@ class json_pointer
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
result.reference_tokens.push_back(std::move(token)); result.reference_tokens.push_back(std::move(token));
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer /// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
@@ -2180,7 +2180,7 @@ class json_pointer
{ {
// create an entry in the array // create an entry in the array
current = &current->operator[](array_index<BasicJsonType>(token)); current = &current->operator[](array_index<BasicJsonType>(token));
break; break; // LCOV_EXCL_LINE (needs an array index with 10 or more digits: more than 1e9 elements)
} }
/* /*
@@ -2217,7 +2217,7 @@ class json_pointer
json_pointer<string_t> result; json_pointer<string_t> result;
result.reference_tokens = reference_tokens; result.reference_tokens = reference_tokens;
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
json_pointer<string_t> convert()&& json_pointer<string_t> convert()&&
{ {
-11
View File
@@ -215,17 +215,6 @@
#define JSON_NO_UNIQUE_ADDRESS #define JSON_NO_UNIQUE_ADDRESS
#endif #endif
// Inlines small functions even in unoptimized builds, so that they are not
// emitted. The parsers for parse(), accept(), and from_*() use it for the
// functions that stand in for the code recovering from errors (see #3989).
// MSVC is left to decide, as it warns (C4714) where it does not inline a
// __forceinline function.
#if defined(_MSC_VER) && !defined(__clang__)
#define JSON_INTERNAL_ALWAYS_INLINE
#else
#define JSON_INTERNAL_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
#endif
// Clang targeting MinGW does not survive the thread_local storage the copy // Clang targeting MinGW does not survive the thread_local storage the copy
// constructor uses to bound its descent: every test that copies a value // constructor uses to bound its descent: every test that copies a value
// segfaults with clang 11.0.1 and clang 18.1.8, while the same tests pass with // segfaults with clang 11.0.1 and clang 18.1.8, while the same tests pass with
@@ -19,7 +19,6 @@
#undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL #undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL
#undef JSON_INLINE_VARIABLE #undef JSON_INLINE_VARIABLE
#undef JSON_NO_UNIQUE_ADDRESS #undef JSON_NO_UNIQUE_ADDRESS
#undef JSON_INTERNAL_ALWAYS_INLINE
#undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION #undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_UNLOCKED_FILE_IO #undef JSON_UNLOCKED_FILE_IO
@@ -833,7 +833,7 @@ class binary_writer
while (n != 0); while (n != 0);
std::reverse(result.begin(), result.end()); std::reverse(result.begin(), result.end());
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/*! /*!
@brief the decimal digits of the integer @a n, with a leading '-' if it @brief the decimal digits of the integer @a n, with a leading '-' if it
@@ -860,7 +860,7 @@ class binary_writer
} }
std::reverse(result.begin(), result.end()); std::reverse(result.begin(), result.end());
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/*! /*!
@brief throws because the integer @a j is too large for @a format @brief throws because the integer @a j is too large for @a format
@@ -1792,7 +1792,7 @@ class binary_writer
std::memcpy(out, &n, sizeof(NumberType)); std::memcpy(out, &n, sizeof(NumberType));
if (!is_little_endian) if (!is_little_endian)
{ {
std::reverse(out, out + sizeof(NumberType)); std::reverse(out, out + sizeof(NumberType)); // LCOV_EXCL_LINE (big-endian hosts only)
} }
} }
@@ -1921,7 +1921,7 @@ class binary_writer
const string_t& value = *j.m_data.m_value.string; const string_t& value = *j.m_data.m_value.string;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size() + 1ul))) if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size() + 1ul)))
{ {
return false; return false; // LCOV_EXCL_LINE (a string of 2 GiB or more)
} }
out[0] = to_char_type(0x02); out[0] = to_char_type(0x02);
store_little_endian<std::int32_t>(p, static_cast<std::int32_t>(value.size() + 1ul)); store_little_endian<std::int32_t>(p, static_cast<std::int32_t>(value.size() + 1ul));
@@ -2378,7 +2378,7 @@ class binary_writer
const std::size_t size = oa.written() - pos; const std::size_t size = oa.written() - pos;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size))) if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
{ {
return false; return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
} }
const auto le_size = static_cast<std::uint32_t>(size); const auto le_size = static_cast<std::uint32_t>(size);
oa.overwrite(pos, is_little_endian ? le_size : byteswap(le_size)); oa.overwrite(pos, is_little_endian ? le_size : byteswap(le_size));
@@ -2422,7 +2422,6 @@ class binary_writer
nothing large has been written then, and the caller writes the nothing large has been written then, and the caller writes the
document in two passes, which reports that error document in two passes, which reports that error
*/ */
template<typename SinkType = OutputSinkType, typename std::enable_if<is_detected<sink_written_t, SinkType>::value, int>::type = 0>
bool write_bson_document_single_pass(const BasicJsonType& document) bool write_bson_document_single_pass(const BasicJsonType& document)
{ {
const std::size_t start = oa.written(); const std::size_t start = oa.written();
@@ -2507,7 +2506,7 @@ class binary_writer
{ {
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(nested_size_pos))) if (JSON_HEDLEY_UNLIKELY(!close_bson_container(nested_size_pos)))
{ {
return false; return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
} }
continue; continue;
} }
@@ -2522,7 +2521,7 @@ class binary_writer
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(current.size_slot))) if (JSON_HEDLEY_UNLIKELY(!close_bson_container(current.size_slot)))
{ {
return false; return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
} }
if (parents.empty()) if (parents.empty())
{ {
@@ -2779,10 +2778,10 @@ class binary_writer
Selected through masks rather than branches, as the type and the sign of Selected through masks rather than branches, as the type and the sign of
the values in an array of mixed signs alternate unpredictably (non-negative the values in an array of mixed signs alternate unpredictably (non-negative
integers are number_unsigned after parsing). Up to 64 bits, the sign is bit integers are number_unsigned after parsing). The sign is bit 63 of the
63 of the result (a narrower integer is sign-extended); computing it with result (a narrower integer is sign-extended); computing it with masks keeps
masks keeps GCC from branching on the type. A wider integer has to be GCC from branching on the type. Only called if @ref integers_fit_64_bits();
compared. a wider number type is written with range checks on the whole value.
*/ */
static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept
{ {
@@ -2790,9 +2789,7 @@ class binary_writer
const std::uint64_t raw = is_signed const std::uint64_t raw = is_signed
? static_cast<std::uint64_t>(j.m_data.m_value.number_integer) ? static_cast<std::uint64_t>(j.m_data.m_value.number_integer)
: static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned); : static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned);
sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t) sign_mask = (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u));
? (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u))
: std::uint64_t{0} - static_cast<std::uint64_t>(is_signed && j.m_data.m_value.number_integer < 0);
return raw; return raw;
} }
@@ -3129,22 +3126,19 @@ class binary_writer
// negative fixnum // negative fixnum
write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer)); write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer));
} }
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int8_t>::min)() && else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int8_t>::min)())
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int8_t>::max)())
{ {
// int 8 // int 8
oa.write_character(to_char_type(0xD0)); oa.write_character(to_char_type(0xD0));
write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer)); write_number(static_cast<std::int8_t>(j.m_data.m_value.number_integer));
} }
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int16_t>::min)() && else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int16_t>::min)())
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int16_t>::max)())
{ {
// int 16 // int 16
oa.write_character(to_char_type(0xD1)); oa.write_character(to_char_type(0xD1));
write_number(static_cast<std::int16_t>(j.m_data.m_value.number_integer)); write_number(static_cast<std::int16_t>(j.m_data.m_value.number_integer));
} }
else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int32_t>::min)() && else if (j.m_data.m_value.number_integer >= (std::numeric_limits<std::int32_t>::min)())
j.m_data.m_value.number_integer <= (std::numeric_limits<std::int32_t>::max)())
{ {
// int 32 // int 32
oa.write_character(to_char_type(0xD2)); oa.write_character(to_char_type(0xD2));
@@ -4060,7 +4054,7 @@ class binary_writer
Containers, strings, high-precision numbers, booleans and null cannot be Containers, strings, high-precision numbers, booleans and null cannot be
declared as the single type of an optimized container in BJData; such a declared as the single type of an optimized container in BJData; such a
container is written unoptimized. The reader rejects them with the same container is written unoptimized. The reader rejects them with the same
list (binary_reader::is_bjd_excluded_optimized_type()). list (binary_reader::is_valid_optimized_type()).
*/ */
static constexpr bool is_bjdata_excluded_type_marker(const CharType marker) noexcept static constexpr bool is_bjdata_excluded_type_marker(const CharType marker) noexcept
{ {
@@ -4159,32 +4153,13 @@ class binary_writer
} }
/*! /*!
@brief validate (dry_run) or write one BJData ND-array element of integer dtype @a T @brief whether the BJData ND-array element @a el can be written with dtype 'd' (single precision)
@return whether @a el's value is in range of @a T; always true when @a dry_run is false
*/
template<typename T>
bool write_bjdata_ndarray_element(const BasicJsonType& el, const bool dry_run)
{
if (dry_run)
{
return bjdata_ndarray_value_in_range<T>(el);
}
using storage_type = typename std::conditional<std::is_unsigned<T>::value, std::uint64_t, std::int64_t>::type;
write_number(static_cast<T>(el.template get<storage_type>()), true);
return true;
}
/*!
@brief validate (dry_run) or write one BJData ND-array element of dtype 'd' (single precision)
@return whether @a el's value survives narrowing to float and back without any change @return whether @a el's value survives narrowing to float and back without any change
(so the ND-array round-trips exactly), or is infinite or NaN; always true when (so the ND-array round-trips exactly), or is infinite or NaN
@a dry_run is false
*/ */
bool write_bjdata_ndarray_float_element(const BasicJsonType& el, const bool dry_run) static bool bjdata_ndarray_float_in_range(const BasicJsonType& el) noexcept
{ {
const auto dval = static_cast<double>(el.m_data.m_value.number_float); const auto dval = static_cast<double>(el.m_data.m_value.number_float);
if (dry_run)
{
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
@@ -4192,7 +4167,7 @@ class binary_writer
// a value that would be rounded (rather than exactly represented) by the // a value that would be rounded (rather than exactly represented) by the
// narrowing to float is treated like an out-of-range integer element; this // narrowing to float is treated like an out-of-range integer element; this
// is the same criterion write_compact_float() uses for CBOR/MessagePack // is the same criterion write_compact_float() uses for CBOR/MessagePack
const bool in_range = std::isnan(dval) || return std::isnan(dval) ||
(dval >= static_cast<double>(std::numeric_limits<float>::lowest()) && (dval >= static_cast<double>(std::numeric_limits<float>::lowest()) &&
dval <= static_cast<double>((std::numeric_limits<float>::max)()) && dval <= static_cast<double>((std::numeric_limits<float>::max)()) &&
static_cast<double>(static_cast<float>(dval)) == dval) || static_cast<double>(static_cast<float>(dval)) == dval) ||
@@ -4200,16 +4175,12 @@ class binary_writer
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_POP JSON_HEDLEY_DIAGNOSTIC_POP
#endif #endif
return in_range;
}
write_number(static_cast<float>(dval), true);
return true;
} }
/*! /*!
@brief write the elements of an ND-array as the number kind @a dtype names @brief write the elements of an ND-array as the number kind @a dtype names
(see @ref write_bjdata_ndarray), after they have been found to be (see @ref write_bjdata_ndarray), after they have been found to be
in range by a dry run of @ref write_bjdata_ndarray_elements in range by @ref bjdata_ndarray_elements_in_range
The elements are encoded in blocks into space reserved from the sink. The elements are encoded in blocks into space reserved from the sink.
*/ */
@@ -4291,20 +4262,13 @@ class binary_writer
} }
/*! /*!
@brief validate or write every element of a BJData ND-array's `_ArrayData_` @brief whether every element of a BJData ND-array's `_ArrayData_` is in range for its dtype
@param[in] array_data the `_ArrayData_` array @param[in] array_data the `_ArrayData_` array
@param[in] dtype the ND-array dtype marker, as returned by bjdata_ndarray_type_marker() @param[in] dtype the ND-array dtype marker, as returned by bjdata_ndarray_type_marker()
@param[in] dry_run true to only range-check each element, false to write it @return whether every element is in range for @a dtype
@return whether every element is in range for @a dtype (always true when @a dry_run is false)
*/ */
bool write_bjdata_ndarray_elements(const BasicJsonType& array_data, const CharType dtype, const bool dry_run) static bool bjdata_ndarray_elements_in_range(const BasicJsonType& array_data, const CharType dtype)
{ {
if (!dry_run)
{
write_bjdata_ndarray_values(array_data, dtype);
return true;
}
for (const auto& el : array_data) for (const auto& el : array_data)
{ {
bool ok = true; bool ok = true;
@@ -4312,43 +4276,39 @@ class binary_writer
{ {
case 'U': case 'U':
case 'B': case 'B':
ok = write_bjdata_ndarray_element<std::uint8_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint8_t>(el);
break; break;
case 'C': case 'C':
// BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges) // BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges)
ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el); ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el);
break; break;
case 'i': case 'i':
ok = write_bjdata_ndarray_element<std::int8_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int8_t>(el);
break; break;
case 'u': case 'u':
ok = write_bjdata_ndarray_element<std::uint16_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint16_t>(el);
break; break;
case 'I': case 'I':
ok = write_bjdata_ndarray_element<std::int16_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int16_t>(el);
break; break;
case 'm': case 'm':
ok = write_bjdata_ndarray_element<std::uint32_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint32_t>(el);
break; break;
case 'l': case 'l':
ok = write_bjdata_ndarray_element<std::int32_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int32_t>(el);
break; break;
case 'M': case 'M':
ok = write_bjdata_ndarray_element<std::uint64_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint64_t>(el);
break; break;
case 'L': case 'L':
ok = write_bjdata_ndarray_element<std::int64_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int64_t>(el);
break; break;
case 'd': case 'd':
ok = write_bjdata_ndarray_float_element(el, dry_run); ok = bjdata_ndarray_float_in_range(el);
break; break;
case 'D': case 'D':
default: default:
// 'D' (double) already spans the full range of number_float_t // 'D' (double) already spans the full range of number_float_t
if (!dry_run)
{
write_number(el.template get<double>(), true);
}
break; break;
} }
if (!ok) if (!ok)
@@ -4435,7 +4395,7 @@ class binary_writer
const auto dim = el.template get<std::uint64_t>(); const auto dim = el.template get<std::uint64_t>();
if (!value_in_range_of<std::size_t>(dim)) if (!value_in_range_of<std::size_t>(dim))
{ {
return true; return true; // LCOV_EXCL_LINE (size_t is narrower than 64 bits)
} }
const auto dim_size = static_cast<std::size_t>(dim); const auto dim_size = static_cast<std::size_t>(dim);
@@ -4485,7 +4445,7 @@ class binary_writer
// wrap (integers) or overflow to infinity (the "single" precision // wrap (integers) or overflow to infinity (the "single" precision
// float) instead of being reported, so such an object falls back to // float) instead of being reported, so such an object falls back to
// a plain object encoding as well // a plain object encoding as well
if (!write_bjdata_ndarray_elements(array_data, dtype, true)) if (!bjdata_ndarray_elements_in_range(array_data, dtype))
{ {
return true; return true;
} }
@@ -4497,7 +4457,7 @@ class binary_writer
write_ubjson(array_size, use_count, use_type, true, true, bjdata_version); write_ubjson(array_size, use_count, use_type, true, true, bjdata_version);
write_bjdata_ndarray_elements(array_data, dtype, false); write_bjdata_ndarray_values(array_data, dtype);
return false; return false;
} }
@@ -4823,8 +4783,8 @@ class binary_writer
run are encoded into space reserved from the sink, as in run are encoded into space reserved from the sink, as in
@ref write_array_elements; an integer that BON8 cannot write (an unsigned @ref write_array_elements; an integer that BON8 cannot write (an unsigned
one above the int64 range, or one of a number type wider than 64 bits) throws one above the int64 range, or one of a number type wider than 64 bits) throws
from write_bon8_value, like any other value that is not a number. Kept out out_of_range.407, as write_bon8_value does. Kept out of line, like
of line, like @ref write_cbor_array. @ref write_cbor_array.
*/ */
template<typename ArrayType> template<typename ArrayType>
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
@@ -4874,10 +4834,10 @@ class binary_writer
string_open = false; string_open = false;
} }
if (unencodable) if (JSON_HEDLEY_UNLIKELY(unencodable))
{ {
write_bon8_value(*it, string_open, depth + 1); // an integer beyond the int64 range, as write_bon8_value would report it
++it; throw_integer_out_of_range(*it, input_format_t::bon8, "int64");
} }
} }
} }
@@ -4912,15 +4872,14 @@ class binary_writer
break; break;
case value_t::number_integer: case value_t::number_integer:
if (JSON_HEDLEY_LIKELY(integer_fits<std::int64_t>(value.m_data.m_value.number_integer))) if (JSON_HEDLEY_UNLIKELY(!integer_fits<std::int64_t>(value.m_data.m_value.number_integer)))
{ {
// beyond 64 bits (number types wider than that)
throw_integer_out_of_range(value, input_format_t::bon8, "int64");
}
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_integer)); write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_integer));
string_open = false; string_open = false;
break; break;
}
// beyond 64 bits (number types wider than that): throws
write_bon8_value(value, string_open, depth + 1);
break;
case value_t::number_float: case value_t::number_float:
write_bon8_float(value.m_data.m_value.number_float); write_bon8_float(value.m_data.m_value.number_float);
@@ -4928,15 +4887,14 @@ class binary_writer
break; break;
case value_t::number_unsigned: case value_t::number_unsigned:
if (JSON_HEDLEY_LIKELY(integer_fits_max<std::int64_t>(value.m_data.m_value.number_unsigned))) if (JSON_HEDLEY_UNLIKELY(!integer_fits_max<std::int64_t>(value.m_data.m_value.number_unsigned)))
{ {
// too large for BON8
throw_integer_out_of_range(value, input_format_t::bon8, "int64");
}
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_unsigned)); write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_unsigned));
string_open = false; string_open = false;
break; break;
}
// too large for BON8: throws
write_bon8_value(value, string_open, depth + 1);
break;
case value_t::object: case value_t::object:
case value_t::array: case value_t::array:
+7 -22
View File
@@ -1113,36 +1113,21 @@ class serializer
{ {
// the code point is at least 0x60 (smaller ones are handled // the code point is at least 0x60 (smaller ones are handled
// above) and is not printable ASCII (which the scanner // above) and is not printable ASCII (which the scanner
// copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII // copies). Under EnsureAscii it is DEL (0x7F): every
// character // well-formed multi-byte character is escaped by
// write_u_escapes() above, so decode() only ever accepts a
// single byte here, and nothing of it was buffered
if (EnsureAscii) if (EnsureAscii)
{ {
JSON_ASSERT(codepoint >= 0x7F); JSON_ASSERT(codepoint == 0x7F);
if (error_handler == error_handler_t::keep) JSON_ASSERT(bytes == bytes_after_last_accept);
{
// this character was buffered as raw bytes below
// in case it turned out to be part of an
// ill-formed sequence (which is kept as is); now
// that it decoded to a well-formed code point,
// undo that and \u-escape it like any other
// character instead
bytes = bytes_after_last_accept;
}
if (codepoint <= 0xFFFF)
{
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint)); write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
} }
else else
{
write_u_escape(bytes, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)));
write_u_escape(bytes, static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu)));
}
}
else
{ {
// copy byte to buffer (all previous bytes of the // copy byte to buffer (all previous bytes of the
// sequence have been copied in the default case below) // sequence have been copied in the default case below)
string_buffer[bytes++] = s[i]; string_buffer[bytes++] = s[i]; // LCOV_EXCL_LINE (well-formed sequences are copied in runs)
} }
reserve_string_buffer(bytes); reserve_string_buffer(bytes);
+1 -1
View File
@@ -139,7 +139,7 @@ inline OutStringType concat(Args && ... args)
str.reserve(concat_length(args...)); str.reserve(concat_length(args...));
concat_into(str, std::forward<Args>(args)...); concat_into(str, std::forward<Args>(args)...);
return str; return str;
} } // LCOV_EXCL_LINE (exception cleanup only)
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
@@ -83,10 +83,6 @@ template<typename StringType>
inline void unescape(StringType& s) inline void unescape(StringType& s)
{ {
auto pos = s.find_first_of('~', 0); auto pos = s.find_first_of('~', 0);
if (pos == StringType::npos)
{
return;
}
StringType result; StringType result;
result.reserve(s.size()); result.reserve(s.size());
+3 -4
View File
@@ -13,7 +13,6 @@
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t #include <cstdint> // uint8_t, uint32_t
#include <string> // string, to_string #include <string> // string, to_string
#include <utility> // move
#include <nlohmann/detail/abi_macros.hpp> #include <nlohmann/detail/abi_macros.hpp>
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
@@ -37,7 +36,7 @@ StringType to_string(std::size_t value)
StringType result; StringType result;
int_to_string(result, value); int_to_string(result, value);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @return a byte as two uppercase hexadecimal digits /// @return a byte as two uppercase hexadecimal digits
inline std::string hex_byte(const std::uint8_t byte) inline std::string hex_byte(const std::uint8_t byte)
@@ -47,7 +46,7 @@ inline std::string hex_byte(const std::uint8_t byte)
result[0] = nibble_to_hex[byte / 16]; result[0] = nibble_to_hex[byte / 16];
result[1] = nibble_to_hex[byte % 16]; result[1] = nibble_to_hex[byte % 16];
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/*! /*!
@brief write a UTF-16 code unit as four lowercase hexadecimal digits @brief write a UTF-16 code unit as four lowercase hexadecimal digits
@@ -391,7 +390,7 @@ inline StringType sanitize_utf8(const StringType& s, const error_handler_t error
} }
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
+2 -1
View File
@@ -1174,8 +1174,9 @@ indent_done:
{ {
++p; ++p;
} }
else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) else if (!negative || NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9'))
{ {
// (without a sign, the dispatch saw a digit)
p = skip_digits(p + 1, e); p = skip_digits(p + 1, e);
} }
else else
+12 -21
View File
@@ -397,9 +397,9 @@ class view_side
return m_view.size(); return m_view.size();
} }
string_view_t string() const string_view_t string() const noexcept
{ {
return m_view.get_string(); return {m_view.m_doc->str(*m_view.m_node), m_view.m_node->len};
} }
bool boolean() const noexcept bool boolean() const noexcept
@@ -428,28 +428,19 @@ class view_side
return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len); return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len);
} }
/// a number, boolean, or null as a basic_json value (no allocation) /// a number as a basic_json value (no allocation); only called for numbers
BasicJsonType scalar() const BasicJsonType scalar() const
{ {
switch (m_view.type()) const value_t t = m_view.type();
if (t == value_t::number_integer)
{ {
case value_t::number_integer:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>()); return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>());
case value_t::number_unsigned:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
case value_t::number_float:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
case value_t::boolean:
return BasicJsonType(m_view.template get<bool>());
case value_t::null:
case value_t::object:
case value_t::array:
case value_t::string:
case value_t::binary:
case value_t::discarded:
default:
return BasicJsonType(nullptr);
} }
if (t == value_t::number_unsigned)
{
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
}
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
} }
/// start at the first element of this array /// start at the first element of this array
@@ -531,9 +522,9 @@ class json_side
return m_json->size(); return m_json->size();
} }
string_view_t string() const string_view_t string() const noexcept
{ {
const auto& s = m_json->template get_ref<const typename BasicJsonType::string_t&>(); const auto& s = *m_json->template get_ptr<const typename BasicJsonType::string_t*>();
return string_view_t(s.data(), s.size()); return string_view_t(s.data(), s.size());
} }
+2 -2
View File
@@ -96,11 +96,11 @@ NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::
} }
if (*q >= 0x80) if (*q >= 0x80)
{ {
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p)); break; // ill-formed UTF-8: reported below
return; // not reached: the scan stopped at ill-formed UTF-8
} }
++q; // a quote, backslash, or control character ++q; // a quote, backslash, or control character
} }
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
} }
inline void check_utf8(const char* s, std::size_t n) inline void check_utf8(const char* s, std::size_t n)
@@ -158,7 +158,7 @@ inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap)
e.moved_cap[n->off] = cap; e.moved_cap[n->off] = cap;
return; return;
} }
reserve_moved(d); // every caller has called reserve_moved() (see above), so neither push_back() can throw
e.moved.push_back(seq); e.moved.push_back(seq);
e.moved_cap.push_back(cap); e.moved_cap.push_back(cap);
n->off = static_cast<std::uint32_t>(e.moved.size() - 1); n->off = static_cast<std::uint32_t>(e.moved.size() - 1);
@@ -243,7 +243,7 @@ inline node* find_parent(const document_data& d, const node* target)
} }
if (target == lo) if (target == lo)
{ {
return nullptr; return nullptr; // LCOV_EXCL_LINE (the root, or a new value's root, which is linked)
} }
for (;;) for (;;)
{ {
+9 -22
View File
@@ -185,10 +185,6 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
end = p.end; end = p.end;
return; return;
} }
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
{
return;
}
// the open brackets: n, then the last element as long as it is a non-empty container // the open brackets: n, then the last element as long as it is a non-empty container
std::size_t closing = 0; std::size_t closing = 0;
std::size_t pos = std::string::npos; std::size_t pos = std::string::npos;
@@ -234,8 +230,7 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
/// the context basic_json puts in front of the message of an exception about /// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then /// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only /// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root)
/// if there is a path)
/// ///
/// This and the throw helpers that take an error_site are templates, so that /// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be /// the path and range code is instantiated only where an exception can be
@@ -256,12 +251,11 @@ std::string diagnostics(const error_site site)
std::string context; std::string context;
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
std::vector<std::string> tokens; std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens); // a value that is not found leaves no tokens, like the root
if (!found || tokens.empty()) static_cast<void>(edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens));
if (!tokens.empty())
{ {
// context is still empty; returning it keeps the copy elided (-Wnrvo) // the root value has no path, but still has a byte range
return context;
}
std::string path; std::string path;
for (const std::string& token : tokens) for (const std::string& token : tokens)
{ {
@@ -269,6 +263,7 @@ std::string diagnostics(const error_site site)
path += detail::escape(token); path += detail::escape(token);
} }
context = concat('(', path, ") "); context = concat('(', path, ") ");
}
#endif #endif
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0; std::size_t start = 0;
@@ -420,10 +415,12 @@ struct throwing_sax
return true; return true;
} }
// LCOV_EXCL_START (throw_parse_failure parses JSON only)
bool binary(binary_t& /*unused*/) bool binary(binary_t& /*unused*/)
{ {
return true; return true;
} }
// LCOV_EXCL_STOP
bool start_object(std::size_t /*unused*/) bool start_object(std::size_t /*unused*/)
{ {
@@ -452,21 +449,11 @@ struct throwing_sax
/// (a template, as in json_sax_dom_parser: the parser passes the concrete /// (a template, as in json_sax_dom_parser: the parser passes the concrete
/// exception type, which is thrown as is) /// exception type, which is thrown as is)
///
/// It is not [[noreturn]]: sax_parse() recovers from an error if
/// parse_error() returns true, and MSVC reports that code as unreachable
/// (C4702) for a handler that never returns. The flag hides that from it.
template<class Exception> template<class Exception>
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const Exception& ex) [[noreturn]] bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const Exception& ex)
{
if (throwing)
{ {
NLOHMANN_VIEW_THROW(ex); NLOHMANN_VIEW_THROW(ex);
} }
return false; // LCOV_EXCL_LINE
}
bool throwing = true;
}; };
/*! /*!
-4
View File
@@ -787,10 +787,6 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t
d.edits.reset(); d.edits.reset();
d.base[2] = nullptr; d.base[2] = nullptr;
d.owned.clear(); d.owned.clear();
if (d.owned_image.empty() || image != d.owned_image.data())
{
d.owned_image.clear();
}
d.arena.clear(); d.arena.clear();
d.indexes.clear(); d.indexes.clear();
d.index_slots.clear(); d.index_slots.clear();
+3 -8
View File
@@ -65,14 +65,9 @@ struct position_source
return token{end - start}; return token{end - start};
} }
/// the range [first, last), or none if either is npos /// the range [first, last)
void set(std::size_t first, std::size_t last) noexcept void set(std::size_t first, std::size_t last) noexcept
{ {
if (first == std::string::npos || last == std::string::npos)
{
clear();
return;
}
start = first; start = first;
end = last; end = last;
} }
@@ -191,11 +186,11 @@ struct position_source
first = n.off; first = n.off;
last = n.off + 4u; last = n.off + 4u;
break; break;
case node_kind::object: case node_kind::object: // LCOV_EXCL_LINE
case node_kind::array: case node_kind::array:
case node_kind::link: case node_kind::link:
default: default:
break; break; // LCOV_EXCL_LINE
} }
// (a crafted image can point past the text) // (a crafted image can point past the text)
if (last == std::string::npos || last > d.size) if (last == std::string::npos || last > d.size)
+16 -33
View File
@@ -47,6 +47,7 @@ class output_buffer
: m_out(sized(out, estimate)) : m_out(sized(out, estimate))
, m_pos(&m_out[0]) , m_pos(&m_out[0])
, m_end(m_pos + m_out.size()) , m_end(m_pos + m_out.size())
, m_reserved((std::max)(estimate, m_out.size()))
{} {}
void finish() void finish()
@@ -116,11 +117,12 @@ class output_buffer
const auto used = static_cast<std::size_t>(m_pos - m_out.data()); const auto used = static_cast<std::size_t>(m_pos - m_out.data());
// (a step does not go beyond the reserved estimate, so that a good // (a step does not go beyond the reserved estimate, so that a good
// estimate is never copied to a larger allocation; not a clamp: the // estimate is never copied to a larger allocation; not a clamp: the
// lower bound used + n + 256 can exceed capacity(), and then it must win) // lower bound used + n + 256 can exceed m_reserved, and then it must win)
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256); const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_reserved), used + n + 256);
if (size > m_out.capacity()) if (size > m_reserved)
{ {
m_out.reserve((std::max)(m_out.capacity() * 2, size)); m_reserved = (std::max)(m_reserved * 2, size);
m_out.reserve(m_reserved);
} }
m_out.resize(size); m_out.resize(size);
m_pos = &m_out[0] + used; m_pos = &m_out[0] + used;
@@ -130,6 +132,9 @@ class output_buffer
StringType& m_out; StringType& m_out;
char* m_pos; char* m_pos;
char* m_end; char* m_end;
/// what was last passed to reserve(), a lower bound of the capacity
/// (StringType need not have a capacity() member)
std::size_t m_reserved;
}; };
/// The length of the run at s that dump() writes unchanged without /// The length of the run at s that dump() writes unchanged without
@@ -641,7 +646,7 @@ class view_serializer
{ {
invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8)
} }
write_codepoint<false>(s[i], s + i, 1); // a quote, a backslash, or a control character write_escaped(s[i]); // a quote, a backslash, or a control character
++i; ++i;
} }
m_out.put('"'); m_out.put('"');
@@ -1389,20 +1394,10 @@ class view_serializer
/// a float node as dump() writes it /// a float node as dump() writes it
void write_float_node(const node& n) void write_float_node(const node& n)
{ {
write_float_node(n, fast_double {}); // (only for types other than double: doubles go through write_double_ahead())
}
void write_float_node(const node& n, std::false_type /*other*/)
{
write_float(float_value<number_float_t>(m_doc, n)); write_float(float_value<number_float_t>(m_doc, n));
} }
void write_float_node(const node& n, std::true_type /*double*/)
{
m_out.reserve(64);
m_out.set_cursor(write_double_at(m_out.cursor(), n));
}
/*! /*!
@brief (doubles) the float at n as dump() writes it, at w (64 bytes of room) @brief (doubles) the float at n as dump() writes it, at w (64 bytes of room)
@@ -1497,10 +1492,11 @@ class view_serializer
{ {
const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
static_cast<void>(dumped); static_cast<void>(dumped);
} } // LCOV_EXCL_LINE (dump() throws: the callers pass only ill-formed UTF-8)
template<bool EnsureAscii> /// write the escape sequence of a quote, a backslash or a control
void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) /// character (the only bytes a run of plain output stops at)
void write_escaped(std::uint32_t codepoint)
{ {
switch (codepoint) switch (codepoint)
{ {
@@ -1528,20 +1524,7 @@ class view_serializer
default: default:
break; break;
} }
if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) write_u_escape(codepoint); // (a control character)
{
if (codepoint <= 0xFFFF)
{
write_u_escape(codepoint);
}
else
{
write_u_escape(0xD7C0u + (codepoint >> 10u));
write_u_escape(0xDC00u + (codepoint & 0x3FFu));
}
return;
}
m_out.put(reinterpret_cast<const char*>(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs)
} }
void write_u_escape(std::uint32_t u) void write_u_escape(std::uint32_t u)
+28 -3
View File
@@ -33,12 +33,37 @@ namespace view
template<typename T> template<typename T>
struct value_tag {}; struct value_tag {};
/// whether basic_json reads T with a dedicated from_json() overload that does
/// not accept a boolean: exactly number_integer_t, number_unsigned_t and
/// number_float_t (the other arithmetic types convert a boolean to 0 or 1)
template<typename T, typename BasicJsonType>
struct rejects_boolean : std::integral_constant < bool,
std::is_same<T, typename BasicJsonType::number_integer_t>::value
|| std::is_same<T, typename BasicJsonType::number_unsigned_t>::value
|| std::is_same<T, typename BasicJsonType::number_float_t>::value >
{};
/// a boolean as T: 0 or 1
template<typename T>
NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& /*d*/, const node& n, std::false_type /*rejects*/)
{
return static_cast<T>((n.flags & node_flags::is_true) != 0);
}
/// a boolean as number_integer_t, number_unsigned_t or number_float_t: type_error.302, as in basic_json
template<typename T>
[[noreturn]] NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& d, const node& n, std::true_type /*rejects*/)
{
throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
}
/*! /*!
@brief the number or boolean of a node converted to an arithmetic type @brief the number or boolean of a node converted to an arithmetic type
As basic_json's get<T>() for arithmetic types: integers and floats are As basic_json's get<T>() for arithmetic types: integers and floats are
converted with static_cast, booleans give 0 or 1, and other types throw converted with static_cast, booleans give 0 or 1 (but throw type_error.302 for
type_error.302. number_integer_t, number_unsigned_t and number_float_t, as basic_json does),
and other types throw type_error.302.
*/ */
template<typename T, typename BasicJsonType> template<typename T, typename BasicJsonType>
NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n) NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n)
@@ -52,7 +77,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod
case value_t::number_float: case value_t::number_float:
return static_cast<T>(float_value<typename BasicJsonType::number_float_t>(d, n)); return static_cast<T>(float_value<typename BasicJsonType::number_float_t>(d, n));
case value_t::boolean: case value_t::boolean:
return static_cast<T>((n.flags & node_flags::is_true) != 0); return boolean_as<T>(d, n, rejects_boolean<T, BasicJsonType> {});
case value_t::null: case value_t::null:
case value_t::object: case value_t::object:
case value_t::array: case value_t::array:
+131 -92
View File
@@ -157,7 +157,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
friend class ::nlohmann::detail::iter_impl; friend class ::nlohmann::detail::iter_impl;
template<typename BasicJsonType, typename CharType, typename OutputSinkType> template<typename BasicJsonType, typename CharType, typename OutputSinkType>
friend class ::nlohmann::detail::binary_writer; friend class ::nlohmann::detail::binary_writer;
template<typename BasicJsonType, typename InputType, typename SAX, bool AllowRecovery> template<typename BasicJsonType, typename InputType, typename SAX>
friend class ::nlohmann::detail::binary_reader; friend class ::nlohmann::detail::binary_reader;
template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType> template<typename BasicJsonType, typename InputAdapterType, bool Collect, typename LexerType>
friend class ::nlohmann::detail::json_sax_dom_parser; friend class ::nlohmann::detail::json_sax_dom_parser;
@@ -400,7 +400,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}}; result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}};
#elif defined(__GNUC__) || defined(__GNUG__) #elif defined(__GNUC__) || defined(__GNUG__)
result["compiler"] = {{"family", "gcc"}, {"version", detail::concat<string_t>( result["compiler"] = {{"family", "gcc"}, {"version", detail::concat<string_t>(
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC__)), '.', detail::to_string<string_t>(static_cast<std::size_t>(__GNUC__)), '.', // LCOV_EXCL_LINE
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_MINOR__)), '.', detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_MINOR__)), '.',
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_PATCHLEVEL__))) detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_PATCHLEVEL__)))
} }
@@ -543,6 +543,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
//////////////////////// ////////////////////////
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
// the type and value of a basic_json without its base class (defined
// below); the destruction helpers of json_value use it as a stand-in
struct data;
/*! /*!
@brief a JSON value @brief a JSON value
@@ -672,14 +676,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
case value_t::binary: case value_t::binary:
binary = create<binary_t>(); binary = create<binary_t>();
break; break;
case value_t::null: case value_t::null: // LCOV_EXCL_LINE
case value_t::boolean: case value_t::boolean:
case value_t::number_integer: case value_t::number_integer:
case value_t::number_unsigned: case value_t::number_unsigned:
case value_t::number_float: case value_t::number_float:
case value_t::discarded: case value_t::discarded:
default: default:
break; break; // LCOV_EXCL_LINE
} }
} }
@@ -719,27 +723,28 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
explicit json_value(array_t* value) noexcept : array(value) {} explicit json_value(array_t* value) noexcept : array(value) {}
private: private:
// raw, allocation-free transfer of m_data from src to dst: no // raw, allocation-free transfer of the type and value from src to
// set_parents()/assert_invariant() (the former is O(#children) per // dst: no set_parents()/assert_invariant() (the former is
// call under JSON_DIAGNOSTICS, which would make the walk below // O(#children) per call under JSON_DIAGNOSTICS, which would make the
// quadratic); dst takes ownership, src is left as value_t::null. // walk below quadratic); dst takes ownership, src is left as
static void take(basic_json& dst, basic_json& src) noexcept // value_t::null.
static void take(data& dst, data& src) noexcept
{ {
dst.m_data.m_type = src.m_data.m_type; dst.m_type = src.m_type;
dst.m_data.m_value = src.m_data.m_value; dst.m_value = src.m_value;
src.m_data.m_type = value_t::null; src.m_type = value_t::null;
} }
// true if v is not an array/object, or is an already-empty one // true if v is not an array/object, or is an already-empty one
static bool has_no_children(const basic_json& v) noexcept static bool has_no_children(const data& v) noexcept
{ {
if (v.m_data.m_type == value_t::array) if (v.m_type == value_t::array)
{ {
return v.m_data.m_value.array->empty(); return v.m_value.array->empty();
} }
if (v.m_data.m_type == value_t::object) if (v.m_type == value_t::object)
{ {
return v.m_data.m_value.object->empty(); return v.m_value.object->empty();
} }
return true; return true;
} }
@@ -748,21 +753,21 @@ private:
// its own, i.e. v can be freed in one go (its destructor then // its own, i.e. v can be freed in one go (its destructor then
// destroys scalars and empty containers only, so it recurses at most // destroys scalars and empty containers only, so it recurses at most
// one level deep); stops at the first structured child it meets // one level deep); stops at the first structured child it meets
static bool all_children_leaves(const basic_json& v) noexcept static bool all_children_leaves(const data& v) noexcept
{ {
// array_t only needs forward iterators (see custom_array_type) // array_t only needs forward iterators (see custom_array_type)
if (v.m_data.m_type == value_t::array) if (v.m_type == value_t::array)
{ {
const auto& a = *v.m_data.m_value.array; const auto& a = *v.m_value.array;
return std::all_of(a.begin(), a.end(), [](const basic_json & el) return std::all_of(a.begin(), a.end(), [](const basic_json & el)
{ {
return has_no_children(el); return has_no_children(el.m_data);
}); });
} }
const auto& o = *v.m_data.m_value.object; const auto& o = *v.m_value.object;
return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el) return std::all_of(o.begin(), o.end(), [](const typename object_t::value_type & el)
{ {
return has_no_children(el.second); return has_no_children(el.second.m_data);
}); });
} }
@@ -794,30 +799,30 @@ private:
// the child of a non-empty array/object v that the walk in // the child of a non-empty array/object v that the walk in
// destroy_container() continues with (see walk_child_it() above) // destroy_container() continues with (see walk_child_it() above)
static basic_json& walk_child(basic_json& v) static data& walk_child(data& v)
{ {
if (v.m_data.m_type == value_t::array) if (v.m_type == value_t::array)
{ {
return v.m_data.m_value.array->back(); return v.m_value.array->back().m_data;
} }
JSON_ASSERT(v.m_data.m_type == value_t::object); JSON_ASSERT(v.m_type == value_t::object);
return walk_child_it(*v.m_data.m_value.object)->second; return walk_child_it(*v.m_value.object)->second.m_data;
} }
// removes walk_child(v) from a non-empty array/object v; this never // removes walk_child(v) from a non-empty array/object v; this never
// allocates, and since it is only ever called when that child is a // allocates, and since it is only ever called when that child is a
// scalar or an already-empty array/object, destroying it never // scalar or an already-empty array/object, destroying it never
// recurses more than one level deep (see destroy() below) // recurses more than one level deep (see destroy() below)
static void pop_walk_child(basic_json& v) static void pop_walk_child(data& v)
{ {
if (v.m_data.m_type == value_t::array) if (v.m_type == value_t::array)
{ {
v.m_data.m_value.array->pop_back(); v.m_value.array->pop_back();
} }
else else
{ {
JSON_ASSERT(v.m_data.m_type == value_t::object); JSON_ASSERT(v.m_type == value_t::object);
v.m_data.m_value.object->erase(walk_child_it(*v.m_data.m_value.object)); v.m_value.object->erase(walk_child_it(*v.m_value.object));
} }
} }
@@ -826,23 +831,23 @@ private:
// recurses at most one level deep); this is the same allocator-based // recurses at most one level deep); this is the same allocator-based
// free the old recursive implementation used, just factored out so // free the old recursive implementation used, just factored out so
// every level of the walk in destroy() can share it // every level of the walk in destroy() can share it
static void free_container(basic_json& v) noexcept static void free_container(data& v) noexcept
{ {
JSON_ASSERT(has_no_children(v) || all_children_leaves(v)); JSON_ASSERT(has_no_children(v) || all_children_leaves(v));
if (v.m_data.m_type == value_t::array) if (v.m_type == value_t::array)
{ {
AllocatorType<array_t> alloc; AllocatorType<array_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.array); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_value.array);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.array, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_value.array, 1);
} }
else else
{ {
JSON_ASSERT(v.m_data.m_type == value_t::object); JSON_ASSERT(v.m_type == value_t::object);
AllocatorType<object_t> alloc; AllocatorType<object_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.object); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_value.object);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.object, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_value.object, 1);
} }
v.m_data.m_type = value_t::null; // avoid a double free if v is later destructed v.m_type = value_t::null; // avoid a double free if v is later destructed
} }
public: public:
@@ -873,14 +878,15 @@ public:
// This json_value is not itself a basic_json, so the top-level // This json_value is not itself a basic_json, so the top-level
// container is first moved into a local stand-in ("cur"); a // container is first moved into a local stand-in ("cur"); a
// default-constructed basic_json has a null pointer in its // default-constructed data has a null pointer in its m_value
// m_value (see data::m_value's initializer), so swapping it // (see data::m_value's initializer), so swapping it with *this
// with *this leaves this union's own pointer null, and it is // leaves this union's own pointer null, and it is never looked
// never looked at or freed a second time. // at or freed a second time. The stand-ins are data, not
basic_json cur; // basic_json, so that destroying a value never constructs a
cur.m_data.m_type = t; // CustomBaseClass, whose constructor may allocate (#5995).
using std::swap; data cur;
swap(cur.m_data.m_value, *this); cur.m_type = t;
std::swap(cur.m_value, *this);
destroy_bounded(cur, 0); destroy_bounded(cur, 0);
} }
@@ -889,7 +895,7 @@ public:
// The first recursion_depth_limit() levels are destroyed by plain // The first recursion_depth_limit() levels are destroyed by plain
// recursion, which is the fastest way and cannot throw; anything // recursion, which is the fastest way and cannot throw; anything
// nested deeper is handed to destroy_walk(), which uses no call stack. // nested deeper is handed to destroy_walk(), which uses no call stack.
static void destroy_bounded(basic_json& v, std::size_t depth) noexcept static void destroy_bounded(data& v, std::size_t depth) noexcept
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{ {
@@ -897,23 +903,23 @@ public:
return; return;
} }
if (v.m_data.m_type == value_t::array) if (v.m_type == value_t::array)
{ {
for (auto& child : *v.m_data.m_value.array) for (auto& child : *v.m_value.array)
{ {
if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object) if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object)
{ {
destroy_bounded(child, depth + 1); destroy_bounded(child.m_data, depth + 1);
} }
} }
} }
else else
{ {
for (auto& child : *v.m_data.m_value.object) for (auto& child : *v.m_value.object)
{ {
if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object) if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object)
{ {
destroy_bounded(child.second, depth + 1); destroy_bounded(child.second.m_data, depth + 1);
} }
} }
} }
@@ -940,18 +946,15 @@ public:
// array/object, which neither allocates nor recurses more // array/object, which neither allocates nor recurses more
// than one level deep. // than one level deep.
// //
// This json_value is not itself a basic_json, so the // Like destroy_container(), the walk keeps the containers it
// top-level container is first moved into a local stand-in // holds in data stand-ins (cur, prev, gp, tmp), which never
// ("cur"); a default-constructed basic_json has a null // construct a CustomBaseClass.
// pointer in its m_value (see data::m_value's initializer), static void destroy_walk(data& top) noexcept
// so swapping it with *this leaves this union's own pointer
// null, and it is never looked at or freed a second time.
static void destroy_walk(basic_json& top) noexcept
{ {
basic_json cur; data cur;
take(cur, top); take(cur, top);
basic_json prev; // value_t::null: no parent data prev; // value_t::null: no parent
// true right after cur was entered: its children have not been // true right after cur was entered: its children have not been
// looked at yet. If none of them has children of its own, cur is // looked at yet. If none of them has children of its own, cur is
@@ -965,7 +968,7 @@ public:
fresh = false; fresh = false;
if (has_no_children(cur) || (fresh_now && all_children_leaves(cur))) if (has_no_children(cur) || (fresh_now && all_children_leaves(cur)))
{ {
if (prev.m_data.m_type == value_t::null) if (prev.m_type == value_t::null)
{ {
free_container(cur); free_container(cur);
return; // back at the top with nothing left to do return; // back at the top with nothing left to do
@@ -974,7 +977,7 @@ public:
// ascend: detach the grandparent link from prev's // ascend: detach the grandparent link from prev's
// walk_child() slot, drop that (now null) slot, free cur // walk_child() slot, drop that (now null) slot, free cur
// (it is empty), then move up one level // (it is empty), then move up one level
basic_json gp; data gp;
take(gp, walk_child(prev)); take(gp, walk_child(prev));
pop_walk_child(prev); pop_walk_child(prev);
@@ -985,7 +988,7 @@ public:
continue; continue;
} }
basic_json& cur_child_ref = walk_child(cur); data& cur_child_ref = walk_child(cur);
if (has_no_children(cur_child_ref)) if (has_no_children(cur_child_ref))
{ {
@@ -997,7 +1000,7 @@ public:
// descend into the non-empty child, reversing the // descend into the non-empty child, reversing the
// link: its slot takes over prev, and the child becomes // link: its slot takes over prev, and the child becomes
// the new cur // the new cur
basic_json tmp; data tmp;
take(tmp, cur_child_ref); take(tmp, cur_child_ref);
take(cur_child_ref, prev); take(cur_child_ref, prev);
take(prev, cur); take(prev, cur);
@@ -1034,14 +1037,14 @@ public:
destroy_container(t); destroy_container(t);
break; break;
case value_t::null: case value_t::null: // LCOV_EXCL_LINE
case value_t::boolean: case value_t::boolean:
case value_t::number_integer: case value_t::number_integer:
case value_t::number_unsigned: case value_t::number_unsigned:
case value_t::number_float: case value_t::number_float:
case value_t::discarded: case value_t::discarded:
default: default:
break; break; // LCOV_EXCL_LINE
} }
} }
}; };
@@ -1214,9 +1217,9 @@ public:
return is_array() ? array_capacity() : object_capacity(); return is_array() ? array_capacity() : object_capacity();
} }
#else #else
static constexpr std::size_t container_capacity() noexcept static constexpr std::size_t container_capacity() noexcept // LCOV_EXCL_LINE
{ {
return detail::unknown_size(); return detail::unknown_size(); // LCOV_EXCL_LINE
} }
#endif #endif
@@ -1402,7 +1405,7 @@ public:
/// into this switch is worth a few percent when copying a value made /// into this switch is worth a few percent when copying a value made
/// mostly of numbers /// mostly of numbers
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
static void copy_leaf_value(const basic_json& src, basic_json& dst) static void copy_leaf_value(const basic_json& src, basic_json& dst) // LCOV_EXCL_LINE
{ {
switch (src.m_data.m_type) switch (src.m_data.m_type)
{ {
@@ -1576,7 +1579,7 @@ public:
auto src_it = src_object.cbegin(); auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object) for (auto& element : *dst.m_data.m_value.object)
{ {
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) if (JSON_HEDLEY_LIKELY(copy_keys_equal(src_it->first, element.first)))
{ {
copy_shallow(src_it->second, element.second, worklist); copy_shallow(src_it->second, element.second, worklist);
++src_it; ++src_it;
@@ -3134,8 +3137,7 @@ public:
{ {
return parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict); return parser(std::move(ia), nullptr, true, ignore_comments, ignore_trailing_commas).sax_parse(sax, strict);
} }
// the SAX parser may recover from errors (see parse_error()) return detail::binary_reader<basic_json, InputAdapterType, SAX>(std::move(ia), format).sax_parse(sax, strict, tag_handler);
return detail::binary_reader<basic_json, InputAdapterType, SAX, true>(std::move(ia), format).sax_parse(sax, strict, tag_handler);
} }
/// serialize @a j into a new vector; @a write receives the vector-backed writer /// serialize @a j into a new vector; @a write receives the vector-backed writer
@@ -4915,6 +4917,30 @@ public:
return {it, res.second}; return {it, res.second};
} }
private:
/// Sets the parents of a value when it goes out of scope, also when an
/// exception is thrown: a container insertion that throws may already
/// have added elements, whose parents must be set for the value to stay
/// valid (basic exception guarantee). Does nothing without JSON_DIAGNOSTICS.
class set_parents_on_exit
{
public:
explicit set_parents_on_exit(basic_json* v) noexcept : m_value(v) {}
set_parents_on_exit(const set_parents_on_exit&) = delete;
set_parents_on_exit& operator=(const set_parents_on_exit&) = delete;
set_parents_on_exit(set_parents_on_exit&&) = delete;
set_parents_on_exit& operator=(set_parents_on_exit&&) = delete;
~set_parents_on_exit()
{
m_value->set_parents();
}
private:
basic_json* m_value;
};
public:
/// Helper for insertion of an iterator /// Helper for insertion of an iterator
/// @note: This uses std::distance to support GCC 4.8, /// @note: This uses std::distance to support GCC 4.8,
/// see https://github.com/nlohmann/json/pull/1257 /// see https://github.com/nlohmann/json/pull/1257
@@ -4924,6 +4950,7 @@ public:
iterator result(this); iterator result(this);
JSON_ASSERT(m_data.m_value.array != nullptr); JSON_ASSERT(m_data.m_value.array != nullptr);
const set_parents_on_exit parents(this);
auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator); auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator);
m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward<Args>(args)...); m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward<Args>(args)...);
result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos; result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos;
@@ -4932,7 +4959,6 @@ public:
// result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val); // result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val);
// but the return value of insert is missing in GCC 4.8, so it is written this way instead. // but the return value of insert is missing in GCC 4.8, so it is written this way instead.
set_parents();
return result; return result;
} }
@@ -4952,10 +4978,18 @@ public:
throw_iterator_from_other_value(this); throw_iterator_from_other_value(this);
} }
// insert to array and return iterator // vector::insert has no effects if copying a single element to the
// end throws; elsewhere, it may already have shifted the elements, so
// copy first and insert by move, which cannot throw but by allocating
if (pos.m_it.array_iterator == m_data.m_value.array->end())
{
return insert_iterator(pos, val); return insert_iterator(pos, val);
} }
basic_json tmp(val);
return insert_iterator(pos, std::move(tmp));
}
/// @brief inserts element into array /// @brief inserts element into array
/// @sa https://json.nlohmann.me/api/basic_json/insert/ /// @sa https://json.nlohmann.me/api/basic_json/insert/
iterator insert(const_iterator pos, basic_json&& val) // NOLINT(performance-unnecessary-value-param) iterator insert(const_iterator pos, basic_json&& val) // NOLINT(performance-unnecessary-value-param)
@@ -5085,8 +5119,8 @@ public:
detail::throw_literal<invalid_iterator>(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); detail::throw_literal<invalid_iterator>(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this);
} }
const set_parents_on_exit parents(this);
m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator);
set_parents();
} }
/// @brief updates a JSON object from another object, overwriting existing keys /// @brief updates a JSON object from another object, overwriting existing keys
@@ -5354,8 +5388,7 @@ public:
/// @name lexicographical comparison operators /// @name lexicographical comparison operators
/// @{ /// @{
// the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR // the same-type cases for non-owning scalars, used by JSON_IMPLEMENT_SCALAR_FAST
// and JSON_IMPLEMENT_SCALAR_FAST
#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ #define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \
case value_t::null: \ case value_t::null: \
return (null_result); \ return (null_result); \
@@ -5377,7 +5410,7 @@ public:
// 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, deep_result, may_descend) \ #define JSON_IMPLEMENT_OPERATOR(op, 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(); \
\ \
@@ -5405,7 +5438,14 @@ public:
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); \
} \ } \
\ \
JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ /* same-type scalars never get here: the inline fast path of the operator handles them */ \
case value_t::null: \
case value_t::boolean: \
case value_t::number_integer: \
case value_t::number_unsigned: \
case value_t::number_float: \
JSON_ASSERT(false); /* NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) */ \
return (unordered_result); \
\ \
case value_t::string: \ case value_t::string: \
return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \
@@ -5446,7 +5486,7 @@ public:
? (number_integer_t(-1) op number_integer_t(1)) \ ? (number_integer_t(-1) op number_integer_t(1)) \
: (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \ : (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \
} \ } \
else if(compares_unordered(lhs, rhs))\ else if(lhs_type == value_t::discarded || rhs_type == value_t::discarded)\
{\ {\
return (unordered_result);\ return (unordered_result);\
}\ }\
@@ -5514,7 +5554,7 @@ public:
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( ==, false, false,
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true) compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_POP JSON_HEDLEY_DIAGNOSTIC_POP
@@ -5528,7 +5568,6 @@ public:
// 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. // we compare types.
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
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) to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
@@ -5540,7 +5579,7 @@ public:
// 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, operator<(lhs_type, rhs_type),
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false) compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
} }
#endif #endif
@@ -5555,7 +5594,7 @@ public:
/// their inlined code at each call site small. Binary values are /// their inlined code at each call site small. Binary values are
/// rare and left to equal_general(). /// rare and left to equal_general().
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
static int equal_leaf(const_reference lhs, const_reference rhs) noexcept static int equal_leaf(const_reference lhs, const_reference rhs) noexcept // LCOV_EXCL_LINE
{ {
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
@@ -6864,7 +6903,7 @@ public:
basic_json result(value_t::object); basic_json result(value_t::object);
json_pointer::flatten("", *this, result); json_pointer::flatten("", *this, result);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief unflatten a previously flattened JSON value /// @brief unflatten a previously flattened JSON value
/// @sa https://json.nlohmann.me/api/basic_json/unflatten/ /// @sa https://json.nlohmann.me/api/basic_json/unflatten/
@@ -7577,7 +7616,7 @@ public:
result.append(path.data(), path.size()); result.append(path.data(), path.size());
diff_append_key(result, key); diff_append_key(result, key);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief the path of the element @a index of the value at @a path /// @brief the path of the element @a index of the value at @a path
static string_t diff_index_path(const string_t& path, const std::size_t index) static string_t diff_index_path(const string_t& path, const std::size_t index)
@@ -7587,7 +7626,7 @@ public:
result.append(path.data(), path.size()); result.append(path.data(), path.size());
diff_append_index(result, index); diff_append_index(result, index);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief append the "remove" operations for the elements of array /// @brief append the "remove" operations for the elements of array
/// @a source from @a index on, and the "add" operations for the /// @a source from @a index on, and the "add" operations for the
@@ -7747,7 +7786,7 @@ public:
// operations // operations
if (source == target) if (source == target)
{ {
return false; return false; // LCOV_EXCL_LINE (equal objects with the same members in the same order return early)
} }
// slow path: the common keys are in a different relative // slow path: the common keys are in a different relative
+3 -1
View File
@@ -1705,10 +1705,12 @@ class basic_json_document
{ {
// (as before: the library's adapter decides what a null stream yields) // (as before: the library's adapter decides what a null stream yields)
detail::view::append_adapter(buf, detail::input_adapter(file)); detail::view::append_adapter(buf, detail::input_adapter(file));
return;
} }
else
{
detail::view::append_file(buf, file); detail::view::append_file(buf, file);
} }
}
#endif #endif
template<typename T> template<typename T>
+414 -2867
View File
File diff suppressed because it is too large. Load diff
+77 -97
View File
@@ -2479,8 +2479,9 @@ indent_done:
{ {
++p; ++p;
} }
else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) else if (!negative || NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9'))
{ {
// (without a sign, the dispatch saw a digit)
p = skip_digits(p + 1, e); p = skip_digits(p + 1, e);
} }
else else
@@ -3549,9 +3550,9 @@ class view_side
return m_view.size(); return m_view.size();
} }
string_view_t string() const string_view_t string() const noexcept
{ {
return m_view.get_string(); return {m_view.m_doc->str(*m_view.m_node), m_view.m_node->len};
} }
bool boolean() const noexcept bool boolean() const noexcept
@@ -3580,28 +3581,19 @@ class view_side
return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len); return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len);
} }
/// a number, boolean, or null as a basic_json value (no allocation) /// a number as a basic_json value (no allocation); only called for numbers
BasicJsonType scalar() const BasicJsonType scalar() const
{ {
switch (m_view.type()) const value_t t = m_view.type();
if (t == value_t::number_integer)
{ {
case value_t::number_integer:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>()); return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>());
case value_t::number_unsigned:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
case value_t::number_float:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
case value_t::boolean:
return BasicJsonType(m_view.template get<bool>());
case value_t::null:
case value_t::object:
case value_t::array:
case value_t::string:
case value_t::binary:
case value_t::discarded:
default:
return BasicJsonType(nullptr);
} }
if (t == value_t::number_unsigned)
{
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
}
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
} }
/// start at the first element of this array /// start at the first element of this array
@@ -3683,9 +3675,9 @@ class json_side
return m_json->size(); return m_json->size();
} }
string_view_t string() const string_view_t string() const noexcept
{ {
const auto& s = m_json->template get_ref<const typename BasicJsonType::string_t&>(); const auto& s = *m_json->template get_ptr<const typename BasicJsonType::string_t*>();
return string_view_t(s.data(), s.size()); return string_view_t(s.data(), s.size());
} }
@@ -4478,14 +4470,9 @@ struct position_source
return token{end - start}; return token{end - start};
} }
/// the range [first, last), or none if either is npos /// the range [first, last)
void set(std::size_t first, std::size_t last) noexcept void set(std::size_t first, std::size_t last) noexcept
{ {
if (first == std::string::npos || last == std::string::npos)
{
clear();
return;
}
start = first; start = first;
end = last; end = last;
} }
@@ -4604,11 +4591,11 @@ struct position_source
first = n.off; first = n.off;
last = n.off + 4u; last = n.off + 4u;
break; break;
case node_kind::object: case node_kind::object: // LCOV_EXCL_LINE
case node_kind::array: case node_kind::array:
case node_kind::link: case node_kind::link:
default: default:
break; break; // LCOV_EXCL_LINE
} }
// (a crafted image can point past the text) // (a crafted image can point past the text)
if (last == std::string::npos || last > d.size) if (last == std::string::npos || last > d.size)
@@ -5027,10 +5014,6 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
end = p.end; end = p.end;
return; return;
} }
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
{
return;
}
// the open brackets: n, then the last element as long as it is a non-empty container // the open brackets: n, then the last element as long as it is a non-empty container
std::size_t closing = 0; std::size_t closing = 0;
std::size_t pos = std::string::npos; std::size_t pos = std::string::npos;
@@ -5076,8 +5059,7 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std:
/// the context basic_json puts in front of the message of an exception about /// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then /// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only /// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (also for the root)
/// if there is a path)
/// ///
/// This and the throw helpers that take an error_site are templates, so that /// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be /// the path and range code is instantiated only where an exception can be
@@ -5098,12 +5080,11 @@ std::string diagnostics(const error_site site)
std::string context; std::string context;
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
std::vector<std::string> tokens; std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens); // a value that is not found leaves no tokens, like the root
if (!found || tokens.empty()) static_cast<void>(edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens));
if (!tokens.empty())
{ {
// context is still empty; returning it keeps the copy elided (-Wnrvo) // the root value has no path, but still has a byte range
return context;
}
std::string path; std::string path;
for (const std::string& token : tokens) for (const std::string& token : tokens)
{ {
@@ -5111,6 +5092,7 @@ std::string diagnostics(const error_site site)
path += detail::escape(token); path += detail::escape(token);
} }
context = concat('(', path, ") "); context = concat('(', path, ") ");
}
#endif #endif
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0; std::size_t start = 0;
@@ -5262,10 +5244,12 @@ struct throwing_sax
return true; return true;
} }
// LCOV_EXCL_START (throw_parse_failure parses JSON only)
bool binary(binary_t& /*unused*/) bool binary(binary_t& /*unused*/)
{ {
return true; return true;
} }
// LCOV_EXCL_STOP
bool start_object(std::size_t /*unused*/) bool start_object(std::size_t /*unused*/)
{ {
@@ -5294,21 +5278,11 @@ struct throwing_sax
/// (a template, as in json_sax_dom_parser: the parser passes the concrete /// (a template, as in json_sax_dom_parser: the parser passes the concrete
/// exception type, which is thrown as is) /// exception type, which is thrown as is)
///
/// It is not [[noreturn]]: sax_parse() recovers from an error if
/// parse_error() returns true, and MSVC reports that code as unreachable
/// (C4702) for a handler that never returns. The flag hides that from it.
template<class Exception> template<class Exception>
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const Exception& ex) [[noreturn]] bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const Exception& ex)
{
if (throwing)
{ {
NLOHMANN_VIEW_THROW(ex); NLOHMANN_VIEW_THROW(ex);
} }
return false; // LCOV_EXCL_LINE
}
bool throwing = true;
}; };
/*! /*!
@@ -5498,7 +5472,7 @@ inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap)
e.moved_cap[n->off] = cap; e.moved_cap[n->off] = cap;
return; return;
} }
reserve_moved(d); // every caller has called reserve_moved() (see above), so neither push_back() can throw
e.moved.push_back(seq); e.moved.push_back(seq);
e.moved_cap.push_back(cap); e.moved_cap.push_back(cap);
n->off = static_cast<std::uint32_t>(e.moved.size() - 1); n->off = static_cast<std::uint32_t>(e.moved.size() - 1);
@@ -5583,7 +5557,7 @@ inline node* find_parent(const document_data& d, const node* target)
} }
if (target == lo) if (target == lo)
{ {
return nullptr; return nullptr; // LCOV_EXCL_LINE (the root, or a new value's root, which is linked)
} }
for (;;) for (;;)
{ {
@@ -6204,11 +6178,11 @@ NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::
} }
if (*q >= 0x80) if (*q >= 0x80)
{ {
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p)); break; // ill-formed UTF-8: reported below
return; // not reached: the scan stopped at ill-formed UTF-8
} }
++q; // a quote, backslash, or control character ++q; // a quote, backslash, or control character
} }
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
} }
inline void check_utf8(const char* s, std::size_t n) inline void check_utf8(const char* s, std::size_t n)
@@ -7802,10 +7776,6 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t
d.edits.reset(); d.edits.reset();
d.base[2] = nullptr; d.base[2] = nullptr;
d.owned.clear(); d.owned.clear();
if (d.owned_image.empty() || image != d.owned_image.data())
{
d.owned_image.clear();
}
d.arena.clear(); d.arena.clear();
d.indexes.clear(); d.indexes.clear();
d.index_slots.clear(); d.index_slots.clear();
@@ -8521,6 +8491,7 @@ class output_buffer
: m_out(sized(out, estimate)) : m_out(sized(out, estimate))
, m_pos(&m_out[0]) , m_pos(&m_out[0])
, m_end(m_pos + m_out.size()) , m_end(m_pos + m_out.size())
, m_reserved((std::max)(estimate, m_out.size()))
{} {}
void finish() void finish()
@@ -8590,11 +8561,12 @@ class output_buffer
const auto used = static_cast<std::size_t>(m_pos - m_out.data()); const auto used = static_cast<std::size_t>(m_pos - m_out.data());
// (a step does not go beyond the reserved estimate, so that a good // (a step does not go beyond the reserved estimate, so that a good
// estimate is never copied to a larger allocation; not a clamp: the // estimate is never copied to a larger allocation; not a clamp: the
// lower bound used + n + 256 can exceed capacity(), and then it must win) // lower bound used + n + 256 can exceed m_reserved, and then it must win)
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256); const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_reserved), used + n + 256);
if (size > m_out.capacity()) if (size > m_reserved)
{ {
m_out.reserve((std::max)(m_out.capacity() * 2, size)); m_reserved = (std::max)(m_reserved * 2, size);
m_out.reserve(m_reserved);
} }
m_out.resize(size); m_out.resize(size);
m_pos = &m_out[0] + used; m_pos = &m_out[0] + used;
@@ -8604,6 +8576,9 @@ class output_buffer
StringType& m_out; StringType& m_out;
char* m_pos; char* m_pos;
char* m_end; char* m_end;
/// what was last passed to reserve(), a lower bound of the capacity
/// (StringType need not have a capacity() member)
std::size_t m_reserved;
}; };
/// The length of the run at s that dump() writes unchanged without /// The length of the run at s that dump() writes unchanged without
@@ -9115,7 +9090,7 @@ class view_serializer
{ {
invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8)
} }
write_codepoint<false>(s[i], s + i, 1); // a quote, a backslash, or a control character write_escaped(s[i]); // a quote, a backslash, or a control character
++i; ++i;
} }
m_out.put('"'); m_out.put('"');
@@ -9863,20 +9838,10 @@ class view_serializer
/// a float node as dump() writes it /// a float node as dump() writes it
void write_float_node(const node& n) void write_float_node(const node& n)
{ {
write_float_node(n, fast_double {}); // (only for types other than double: doubles go through write_double_ahead())
}
void write_float_node(const node& n, std::false_type /*other*/)
{
write_float(float_value<number_float_t>(m_doc, n)); write_float(float_value<number_float_t>(m_doc, n));
} }
void write_float_node(const node& n, std::true_type /*double*/)
{
m_out.reserve(64);
m_out.set_cursor(write_double_at(m_out.cursor(), n));
}
/*! /*!
@brief (doubles) the float at n as dump() writes it, at w (64 bytes of room) @brief (doubles) the float at n as dump() writes it, at w (64 bytes of room)
@@ -9971,10 +9936,11 @@ class view_serializer
{ {
const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
static_cast<void>(dumped); static_cast<void>(dumped);
} } // LCOV_EXCL_LINE (dump() throws: the callers pass only ill-formed UTF-8)
template<bool EnsureAscii> /// write the escape sequence of a quote, a backslash or a control
void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) /// character (the only bytes a run of plain output stops at)
void write_escaped(std::uint32_t codepoint)
{ {
switch (codepoint) switch (codepoint)
{ {
@@ -10002,20 +9968,7 @@ class view_serializer
default: default:
break; break;
} }
if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) write_u_escape(codepoint); // (a control character)
{
if (codepoint <= 0xFFFF)
{
write_u_escape(codepoint);
}
else
{
write_u_escape(0xD7C0u + (codepoint >> 10u));
write_u_escape(0xDC00u + (codepoint & 0x3FFu));
}
return;
}
m_out.put(reinterpret_cast<const char*>(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs)
} }
void write_u_escape(std::uint32_t u) void write_u_escape(std::uint32_t u)
@@ -10077,12 +10030,37 @@ namespace view
template<typename T> template<typename T>
struct value_tag {}; struct value_tag {};
/// whether basic_json reads T with a dedicated from_json() overload that does
/// not accept a boolean: exactly number_integer_t, number_unsigned_t and
/// number_float_t (the other arithmetic types convert a boolean to 0 or 1)
template<typename T, typename BasicJsonType>
struct rejects_boolean : std::integral_constant < bool,
std::is_same<T, typename BasicJsonType::number_integer_t>::value
|| std::is_same<T, typename BasicJsonType::number_unsigned_t>::value
|| std::is_same<T, typename BasicJsonType::number_float_t>::value >
{};
/// a boolean as T: 0 or 1
template<typename T>
NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& /*d*/, const node& n, std::false_type /*rejects*/)
{
return static_cast<T>((n.flags & node_flags::is_true) != 0);
}
/// a boolean as number_integer_t, number_unsigned_t or number_float_t: type_error.302, as in basic_json
template<typename T>
[[noreturn]] NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& d, const node& n, std::true_type /*rejects*/)
{
throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
}
/*! /*!
@brief the number or boolean of a node converted to an arithmetic type @brief the number or boolean of a node converted to an arithmetic type
As basic_json's get<T>() for arithmetic types: integers and floats are As basic_json's get<T>() for arithmetic types: integers and floats are
converted with static_cast, booleans give 0 or 1, and other types throw converted with static_cast, booleans give 0 or 1 (but throw type_error.302 for
type_error.302. number_integer_t, number_unsigned_t and number_float_t, as basic_json does),
and other types throw type_error.302.
*/ */
template<typename T, typename BasicJsonType> template<typename T, typename BasicJsonType>
NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n) NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n)
@@ -10096,7 +10074,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod
case value_t::number_float: case value_t::number_float:
return static_cast<T>(float_value<typename BasicJsonType::number_float_t>(d, n)); return static_cast<T>(float_value<typename BasicJsonType::number_float_t>(d, n));
case value_t::boolean: case value_t::boolean:
return static_cast<T>((n.flags & node_flags::is_true) != 0); return boolean_as<T>(d, n, rejects_boolean<T, BasicJsonType> {});
case value_t::null: case value_t::null:
case value_t::object: case value_t::object:
case value_t::array: case value_t::array:
@@ -11809,10 +11787,12 @@ class basic_json_document
{ {
// (as before: the library's adapter decides what a null stream yields) // (as before: the library's adapter decides what a null stream yields)
detail::view::append_adapter(buf, detail::input_adapter(file)); detail::view::append_adapter(buf, detail::input_adapter(file));
return;
} }
else
{
detail::view::append_file(buf, file); detail::view::append_file(buf, file);
} }
}
#endif #endif
template<typename T> template<typename T>
+18
View File
@@ -629,6 +629,22 @@ static json make_containers()
return j; return j;
} }
/// many arrays of one number each: one-element arrays are left to the general
/// path (CBOR and MessagePack), which reads a lone number slower than the loop
/// for arrays of scalars would
static json make_singles()
{
json data = json::array();
for (std::size_t i = 0; i < 100000; ++i)
{
data.push_back(json::array({i}));
}
json j = json::object();
j["data"] = std::move(data);
return j;
}
/// one flat array of numbers: the scalar decoding path, which must not move /// one flat array of numbers: the scalar decoding path, which must not move
static json make_scalars() static json make_scalars()
{ {
@@ -676,6 +692,8 @@ BENCHMARK_CAPTURE(FromBinaryShape, containers / ubjson_optimized, make_container
BENCHMARK_CAPTURE(FromBinaryShape, containers / bjdata, make_containers, binary_format::bjdata); BENCHMARK_CAPTURE(FromBinaryShape, containers / bjdata, make_containers, binary_format::bjdata);
BENCHMARK_CAPTURE(FromBinaryShape, containers / bson, make_containers, binary_format::bson); BENCHMARK_CAPTURE(FromBinaryShape, containers / bson, make_containers, binary_format::bson);
BENCHMARK_CAPTURE(FromBinaryShape, containers / bon8, make_containers, binary_format::bon8); BENCHMARK_CAPTURE(FromBinaryShape, containers / bon8, make_containers, binary_format::bon8);
BENCHMARK_CAPTURE(FromBinaryShape, singles / cbor, make_singles, binary_format::cbor);
BENCHMARK_CAPTURE(FromBinaryShape, singles / msgpack, make_singles, binary_format::msgpack);
// BSON names every array element, so a large array measures key generation // BSON names every array element, so a large array measures key generation
// rather than scalar decoding and is left out here // rather than scalar decoding and is left out here
BENCHMARK_CAPTURE(FromBinaryShape, scalars / cbor, make_scalars, binary_format::cbor); BENCHMARK_CAPTURE(FromBinaryShape, scalars / cbor, make_scalars, binary_format::cbor);
-13
View File
@@ -47,10 +47,6 @@ dumps is stable under exactly the same values that break operator==.
The unit tests run the same checks on a fixed corpus (see the "BJData round-trip The unit tests run the same checks on a fixed corpus (see the "BJData round-trip
invariants" test case), so keep both in sync. invariants" test case), so keep both in sync.
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_bjdata() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -61,7 +57,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -86,9 +81,6 @@ static bool is_value_stable(const json& lhs, const json& rhs)
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::bjdata).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -107,9 +99,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2.1: round trip without adding size annotations to container types // step 2.1: round trip without adding size annotations to container types
@@ -143,7 +132,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -154,7 +142,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_bon8(vec) - j2 = from_bon8(vec)
- assert(to_bon8(j2) == vec) - assert(to_bon8(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_bon8() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_bon8
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::bon8).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_bson(vec) - j2 = from_bson(vec)
- assert(to_bson(j2) == vec) - assert(to_bson(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_bson() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_bson
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::bson).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors can occur during parsing, too // out of range errors can occur during parsing, too
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_cbor(vec) - j2 = from_cbor(vec)
- assert(to_cbor(j2) == vec) - assert(to_cbor(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_cbor() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_cbor
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::cbor).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors can occur during parsing, too // out of range errors can occur during parsing, too
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-12
View File
@@ -24,16 +24,11 @@ array data, it performs the following steps:
The pointer range takes the bulk fast paths, the other input adapters read byte The pointer range takes the bulk fast paths, the other input adapters read byte
by byte; see check_input_adapters(). by byte; see check_input_adapters().
Furthermore, it parses data with a SAX parser that recovers from every error
and checks that the events are balanced, that parsing ends, and that valid
input is parsed without errors (see #3989).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers. drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -64,13 +59,6 @@ struct accept_json
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
{
const auto checker = check_recovering_parse(data, size, json::input_format_t::json);
assert(checker.events <= (4 * size) + 4);
assert((checker.errors == 0) == json::accept(data, data + size));
}
// step 0: parse input without exceptions // step 0: parse input without exceptions
bool noexcept_threw = false; bool noexcept_threw = false;
json const j_noexcept = parse_without_exceptions([&] { return json::parse(data, data + size, nullptr, false); }, noexcept_threw); json const j_noexcept = parse_without_exceptions([&] { return json::parse(data, data + size, nullptr, false); }, noexcept_threw);
-13
View File
@@ -17,10 +17,6 @@ array data, it performs the following steps:
- j2 = from_msgpack(vec) - j2 = from_msgpack(vec)
- assert(to_msgpack(j2) == vec) - assert(to_msgpack(j2) == vec)
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_msgpack() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -31,7 +27,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -49,9 +44,6 @@ struct read_msgpack
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::msgpack).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -70,9 +62,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2: round trip // step 2: round trip
@@ -94,7 +83,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -105,7 +93,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-13
View File
@@ -26,10 +26,6 @@ array data, it performs the following steps:
The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip
invariants" test case), so keep both in sync. invariants" test case), so keep both in sync.
Furthermore, it reads data with a SAX parser that recovers from every error
and checks that the events are balanced, that reading ends, and that it
reports an error exactly when from_ubjson() fails (see #3989).
Finally, it reads the data from a stream, a non-contiguous iterator range, and Finally, it reads the data from a stream, a non-contiguous iterator range, and
a FILE*, which read byte by byte, and checks that each gives the same value or a FILE*, which read byte by byte, and checks that each gives the same value or
error (id, message, byte position) as the pointer range, which takes the bulk error (id, message, byte position) as the pointer range, which takes the bulk
@@ -40,7 +36,6 @@ drivers.
*/ */
#include "fuzzer_common.hpp" #include "fuzzer_common.hpp"
#include "fuzzer-recovering_checker.hpp"
namespace namespace
{ {
@@ -58,9 +53,6 @@ struct read_ubjson
// see http://llvm.org/docs/LibFuzzer.html // see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// recover from all errors, reading from memory and from a stream
const bool recovered_without_errors = check_recovering_parse(data, size, json::input_format_t::ubjson).errors == 0;
std::vector<uint8_t> const vec1(data, data + size); std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions // step 0: parse input without exceptions
@@ -79,9 +71,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
// without exceptions, the same input must give the same value // without exceptions, the same input must give the same value
assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1)); assert(!noexcept_threw && !j_noexcept.is_discarded() && same_value(j_noexcept, j1));
// the recovering parser must not have reported an error either
assert(recovered_without_errors);
try try
{ {
// step 2.1: round trip without adding size annotations to container types // step 2.1: round trip without adding size annotations to container types
@@ -113,7 +102,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// parse errors are ok, because input may be random bytes // parse errors are ok, because input may be random bytes
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
catch (const json::type_error&) catch (const json::type_error&)
{ {
@@ -124,7 +112,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{ {
// out of range errors may happen if provided sizes are excessive // out of range errors may happen if provided sizes are excessive
assert(parsed || noexcept_threw || j_noexcept.is_discarded()); assert(parsed || noexcept_threw || j_noexcept.is_discarded());
assert(parsed || !recovered_without_errors);
} }
// step 3: the other input adapters must read the data alike // step 3: the other input adapters must read the data alike
-154
View File
@@ -1,154 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
namespace
{
// a SAX parser that recovers from every error and checks that the events are
// balanced and that every key is followed by exactly one value
class recovering_checker : public nlohmann::json_sax<nlohmann::json>
{
public:
bool null() override
{
return value();
}
bool boolean(bool /*val*/) override
{
return value();
}
bool number_integer(number_integer_t /*val*/) override
{
return value();
}
bool number_unsigned(number_unsigned_t /*val*/) override
{
return value();
}
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
{
return value();
}
bool string(string_t& /*val*/) override
{
return value();
}
bool binary(binary_t& /*val*/) override
{
return value();
}
bool start_object(std::size_t /*elements*/) override
{
value();
stack.push_back('o');
return true;
}
bool key(string_t& /*val*/) override
{
++events;
assert(!stack.empty() && stack.back() == 'o');
stack.back() = 'v';
return true;
}
bool end_object() override
{
++events;
assert(!stack.empty() && stack.back() == 'o');
stack.pop_back();
return true;
}
bool start_array(std::size_t /*elements*/) override
{
value();
stack.push_back('a');
return true;
}
bool end_array() override
{
++events;
assert(!stack.empty() && stack.back() == 'a');
stack.pop_back();
return true;
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
{
++errors;
return true;
}
bool complete() const
{
return stack.empty();
}
std::size_t events = 0;
std::size_t errors = 0;
private:
bool value()
{
++events;
if (!stack.empty())
{
// an array element, or the value of a key
assert(stack.back() != 'o');
if (stack.back() == 'v')
{
stack.back() = 'o';
}
}
return true;
}
// 'a' for an array, 'o' for an object that expects a key, 'v' for an
// object that expects the value of a key
std::vector<char> stack {}; // NOLINT(readability-redundant-member-init)
};
/// parses @a data with a recovering_checker from memory and from a stream,
/// checks that both see the same, that the events are balanced, and that the
/// number of errors is bounded, and returns the checker (see #3989)
inline recovering_checker check_recovering_parse(const std::uint8_t* data, const std::size_t size, const nlohmann::json::input_format_t format)
{
recovering_checker checker;
const bool ok = nlohmann::json::sax_parse(data, data + size, &checker, format);
assert(checker.complete());
assert(checker.errors <= size + 1);
assert(ok == (checker.errors == 0));
std::istringstream stream(std::string(reinterpret_cast<const char*>(data), size));
recovering_checker stream_checker;
assert(nlohmann::json::sax_parse(stream, &stream_checker, format) == ok);
assert(stream_checker.complete());
assert(stream_checker.events == checker.events);
assert(stream_checker.errors == checker.errors);
return checker;
}
} // namespace
+1 -3
View File
@@ -131,13 +131,11 @@ struct SaxEventLogger : public nlohmann::json_sax<nlohmann::json>
{ {
errored = true; errored = true;
events.push_back("parse_error(" + std::to_string(position) + ")"); events.push_back("parse_error(" + std::to_string(position) + ")");
return recover; return false;
} }
std::vector<std::string> events {}; // NOLINT(readability-redundant-member-init) std::vector<std::string> events {}; // NOLINT(readability-redundant-member-init)
bool errored = false; bool errored = false;
/// whether parse_error() asks the parser to recover from the error (see #3989)
bool recover = false;
}; };
struct SaxEventLoggerExitAfterStartObject : public SaxEventLogger struct SaxEventLoggerExitAfterStartObject : public SaxEventLogger
+391 -1
View File
@@ -12,6 +12,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
#include <functional>
#include <limits>
#include <valarray> #include <valarray>
#if JSON_HAS_RANGES #if JSON_HAS_RANGES
#include <ranges> #include <ranges>
@@ -790,7 +792,7 @@ TEST_CASE("a failed allocation leaves the value unchanged")
#if JSON_HAS_RANGES && !defined(__MINGW32__) #if JSON_HAS_RANGES && !defined(__MINGW32__)
const std::vector<int> numbers = {1, 2}; const std::vector<int> numbers = {1, 2};
next_construct_fails = true; next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/) CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/) noexcept
{ {
return true; return true;
})), std::bad_alloc&); })), std::bad_alloc&);
@@ -804,3 +806,391 @@ TEST_CASE("a failed allocation leaves the value unchanged")
#endif #endif
} }
#endif #endif
// the no-exceptions CI job has no std::bad_alloc to catch; with iterator
// debugging (MSVC STL debug builds) containers allocate inside noexcept
// functions, so failing an allocation there terminates the program
#if !defined(JSON_NOEXCEPTION) && !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
namespace
{
// whether every child of j names j as its parent (always true without
// JSON_DIAGNOSTICS, where no parents are stored)
template<class BasicJsonType>
bool children_name_their_parent(const BasicJsonType& j)
{
#if JSON_DIAGNOSTICS
if (j.is_array())
{
for (const auto& element : *j.m_data.m_value.array)
{
if (element.m_parent != &j)
{
return false;
}
}
}
else if (j.is_object())
{
for (const auto& element : *j.m_data.m_value.object)
{
if (element.second.m_parent != &j)
{
return false;
}
}
}
#else
static_cast<void>(j);
#endif
return true;
}
enum class guarantee
{
basic, // the value stays valid and keeps its type
strong // the value is unchanged
};
// Runs op on a fresh value make() with the 0th, 1st, 2nd, ... allocation made
// by the library failing in turn, until op completes. Every failed attempt
// must have thrown std::bad_alloc and left the value valid, with all parent
// pointers intact (copying a value asserts the invariant), and for the strong
// guarantee exactly as it was.
template<class BasicJsonType>
void check_exception_guarantee(guarantee g,
const std::function<BasicJsonType()>& make,
const std::function<void(BasicJsonType&)>& op)
{
fail_at_alloc_call = -1;
bool completed = false;
std::size_t failures = 0;
for (std::size_t n = 0; !completed; ++n)
{
CAPTURE(n)
REQUIRE(n < 1000);
BasicJsonType j = make();
const std::string before = j.dump();
const auto type_before = j.type();
alloc_call_count = 0;
fail_at_alloc_call = static_cast<long>(n);
try
{
op(j);
completed = true;
}
catch (const std::bad_alloc&)
{
++failures;
}
fail_at_alloc_call = -1;
if (!completed)
{
CHECK((j.type() == type_before));
CHECK(children_name_their_parent(j));
const BasicJsonType copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is the point, it allocates
CHECK(copy.dump() == j.dump());
if (g == guarantee::strong)
{
CHECK(j.dump() == before);
}
}
}
// the operation allocates, so at least one attempt must have failed
CHECK(failures > 0);
}
template<class BasicJsonType>
void check_insert_exception_guarantees()
{
using json_t = BasicJsonType;
using make_t = std::function<json_t()>;
// strings of this length do not fit into the small string buffer, so
// copying them allocates
const json_t val = std::string(64, 'x');
const json_t source = json_t::array({std::string(64, 'p'), std::string(64, 'q'), std::string(64, 'r')});
for (const bool spare_capacity :
{
false, true
})
{
CAPTURE(spare_capacity)
// [ "aaa...", "bbb...", "ccc..." ], with a spare slot if requested
const make_t make = [spare_capacity]()
{
json_t j = json_t::array({std::string(64, 'a'), std::string(64, 'b'), std::string(64, 'c')});
if (spare_capacity)
{
j.push_back(std::string(64, 'd'));
j.erase(3);
}
return json_t(std::move(j));
};
for (const bool at_end :
{
false, true
})
{
CAPTURE(at_end)
SECTION("insert(pos, const basic_json&)")
{
check_exception_guarantee<json_t>(guarantee::strong, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, val);
});
}
SECTION("insert(pos, basic_json&&)")
{
check_exception_guarantee<json_t>(guarantee::strong, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, json_t(val));
});
}
SECTION("insert(pos, size_type, const basic_json&)")
{
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, 3, val);
});
}
SECTION("insert(pos, const_iterator, const_iterator)")
{
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, source.begin(), source.end());
});
}
SECTION("insert(pos, initializer_list_t)")
{
check_exception_guarantee<json_t>(guarantee::basic, make, [&](json_t& j)
{
j.insert(at_end ? j.end() : j.begin() + 1, {val, val});
});
}
}
}
SECTION("insert(const_iterator, const_iterator) for objects")
{
const json_t members = {{"m", std::string(64, 'm')}, {"n", std::string(64, 'n')}, {"o", std::string(64, 'o')}};
SECTION("no member exists yet")
{
check_exception_guarantee<json_t>(guarantee::basic, []()
{
return json_t({{"a", 1}});
}, [&](json_t& j)
{
j.insert(members.begin(), members.end());
});
}
SECTION("some members exist already")
{
check_exception_guarantee<json_t>(guarantee::basic, []()
{
return json_t({{"a", 1}, {"n", std::string(64, 'x')}});
}, [&](json_t& j)
{
j.insert(members.begin(), members.end());
});
}
}
}
} // namespace
TEST_CASE("insert() provides its documented exception guarantees (#5986)")
{
SECTION("std::map-backed object_t")
{
check_insert_exception_guarantees<nlohmann::json::with_allocator_t<nth_alloc_fails_allocator>>();
}
SECTION("ordered_map-backed object_t")
{
check_insert_exception_guarantees<nlohmann::ordered_json::with_allocator_t<nth_alloc_fails_allocator>>();
}
}
#endif
#if !defined(JSON_NOEXCEPTION)
namespace
{
// the allocation numbered sweep_fail_at (counted from 1) throws; 0 never fails
std::size_t sweep_allocations = 0;
std::size_t sweep_fail_at = 0;
std::ptrdiff_t sweep_live = 0;
template<class T>
struct sweep_allocator
{
using value_type = T;
sweep_allocator() = default;
template<class U> sweep_allocator(const sweep_allocator<U>& /*unused*/) noexcept {}
T* allocate(std::size_t n)
{
if (sweep_fail_at != 0 && ++sweep_allocations == sweep_fail_at)
{
throw std::bad_alloc();
}
++sweep_live;
return std::allocator<T>().allocate(n);
}
void deallocate(T* p, std::size_t n) noexcept
{
--sweep_live;
std::allocator<T>().deallocate(p, n);
}
// GCC 4.8's std::map calls these on the allocator directly instead of
// going through std::allocator_traits
std::size_t max_size() const noexcept
{
return (std::numeric_limits<std::size_t>::max)() / sizeof(T);
}
template<class U, class... Args>
void construct(U* p, Args&& ... args) noexcept(noexcept(::new (static_cast<void*>(p)) U(std::forward<Args>(args)...)))
{
::new (static_cast<void*>(p)) U(std::forward<Args>(args)...);
}
template<class U>
void destroy(U* p) noexcept
{
static_cast<void>(p); // unused for trivially destructible types
p->~U();
}
template <class U>
struct rebind
{
using other = sweep_allocator<U>;
};
bool operator==(const sweep_allocator& /*unused*/) const noexcept
{
return true;
}
bool operator!=(const sweep_allocator& /*unused*/) const noexcept
{
return false;
}
};
// run make() with the allocation numbered k failing, for k = 1, 2, ...: every
// failure must be a std::bad_alloc and must not leave anything behind, and
// the first k without a failure must yield the value made without failures
template<class BasicJsonType, class Make>
void sweep_allocations_of(const Make& make)
{
sweep_fail_at = 0;
const BasicJsonType expected = make();
const std::ptrdiff_t live_before = sweep_live;
std::size_t failures = 0;
for (std::size_t k = 1; k < 10000; ++k)
{
sweep_allocations = 0;
sweep_fail_at = k;
try
{
const BasicJsonType result = make();
sweep_fail_at = 0;
CHECK(result == expected);
break;
}
catch (const std::bad_alloc&)
{
sweep_fail_at = 0;
++failures;
}
CHECK(sweep_live == live_before);
}
sweep_fail_at = 0;
CHECK(failures > 10);
CHECK(sweep_live == live_before);
}
} // namespace
TEST_CASE("parse() survives a failing allocation at every point (#5985)")
{
// strings beyond the small string buffer, nested objects and arrays
const std::string text = R"({"a":"a long string that does not fit into the small string buffer1","b":[1,2,[3,{"k":"another string that is longer than any small string buffer"}]],"c":{"x":1,"y":{"z":[]},"w":"yet another string that does not fit into a small buffer"},"d":1,"b":[[],{}]})";
using sweep_json = nlohmann::json::with_allocator_t<sweep_allocator>;
using sweep_ordered_json = nlohmann::ordered_json::with_allocator_t<sweep_allocator>;
SECTION("json")
{
sweep_allocations_of<sweep_json>([&]
{
return sweep_json::parse(text);
});
}
SECTION("ordered_json")
{
sweep_allocations_of<sweep_ordered_json>([&]
{
return sweep_ordered_json::parse(text);
});
}
// With iterator debugging (MSVC STL debug builds, also used by clang-cl),
// containers allocate a debug proxy through the allocator inside their
// noexcept move constructors, so failing that allocation terminates the
// program instead of throwing std::bad_alloc. Nothing to check there.
#if !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
SECTION("parser callback")
{
const auto keep_all = [](int /*depth*/, sweep_json::parse_event_t /*event*/, sweep_json& /*parsed*/) noexcept
{
return true;
};
sweep_allocations_of<sweep_json>([&]
{
return sweep_json::parse(text, keep_all);
});
const auto drop_d = [](int /*depth*/, sweep_json::parse_event_t event, sweep_json & parsed)
{
return event != sweep_json::parse_event_t::key || parsed != "d";
};
sweep_allocations_of<sweep_json>([&]
{
return sweep_json::parse(text, drop_d);
});
}
#endif
SECTION("sax_parse into an existing value")
{
// a failed parse leaves the target in a state that is safe to destroy
using sax_t = nlohmann::detail::json_sax_dom_parser<sweep_json, nlohmann::detail::iterator_input_adapter<const char*>>;
sweep_allocations_of<sweep_json>([&]() -> sweep_json
{
sweep_json value;
sax_t sax(value, false);
static_cast<void>(sweep_json::sax_parse(text, &sax));
return value;
});
}
}
#endif
+53 -40
View File
@@ -10,6 +10,7 @@
#include "doctest_compatibility.h" #include "doctest_compatibility.h"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
@@ -432,46 +433,6 @@ TEST_CASE("alternative string type")
CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})"); CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})");
} }
SECTION("error recovery")
{
// a SAX parser that recovers from every error (see #3989)
struct recovering_parser : nlohmann::detail::json_sax_dom_parser<alt_json>
{
explicit recovering_parser(alt_json& j)
: nlohmann::detail::json_sax_dom_parser<alt_json>(j, false)
{}
// sax_parse() calls the SAX parser's own parse_error(), so hiding
// the one of the base class is what recovering takes
// NOLINTNEXTLINE(bugprone-derived-method-shadowing-base-method)
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const nlohmann::detail::exception& /*unused*/)
{
++errors;
return true;
}
std::size_t errors = 0;
};
alt_json j;
recovering_parser sax(j);
// not inside CHECK(): MSVC reads the escape in a stringized raw string
const std::string input = R"([1., "a\qb", tru, {"k" 2}])";
CHECK(!alt_json::sax_parse(input, &sax));
CHECK(sax.errors == 4);
CHECK(j.dump() == R"([1,"aqb",null,{"k":2}])");
// a UBJSON high-precision number, a CBOR key that is not a string
alt_json u;
recovering_parser ubjson_sax(u);
CHECK(!alt_json::sax_parse(std::vector<std::uint8_t> {'[', 'H', 'i', 2, '1', '.', ']'}, &ubjson_sax, alt_json::input_format_t::ubjson));
CHECK(u.dump() == "[1]");
alt_json c;
recovering_parser cbor_sax(c);
CHECK(!alt_json::sax_parse(std::vector<std::uint8_t> {0xA2, 0x01, 0x02, 0x61, 'a', 0x03}, &cbor_sax, alt_json::input_format_t::cbor));
CHECK(c.dump() == R"({"a":3})");
}
SECTION("conversion between basic_json specializations (#2649)") SECTION("conversion between basic_json specializations (#2649)")
{ {
// explicit conversions are always possible // explicit conversions are always possible
@@ -523,4 +484,56 @@ TEST_CASE("alternative string type")
} }
} }
TEST_CASE("alternative string type with json_view")
{
// regression test for #5994: basic_json_view::dump() called
// StringType::capacity(), which alt_string (and the documented StringType
// requirements) do not provide
using alt_document = nlohmann::basic_json_document<alt_json, nlohmann::view_mode::read_only>;
using alt_editable_document = nlohmann::basic_json_document<alt_json, nlohmann::view_mode::editable>;
const char* text = R"({"a":[1,"x\u00e4"],"b":{"c":true,"d":null},"e":2.5})";
const alt_json j = alt_json::parse(text);
SECTION("read-only document")
{
const alt_document d = alt_document::parse(text);
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(2) == j.dump(2));
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().materialize() == j);
CHECK(d.root().contains(alt_string("b")));
CHECK(d.root()[alt_string("b")][alt_string("c")].dump() == "true");
}
SECTION("long output")
{
// more than one growth step of the output buffer
alt_json big = alt_json::array();
for (int i = 0; i < 20000; ++i)
{
big.push_back(alt_json::parse(text));
}
const alt_string big_text = big.dump();
const alt_document d = alt_document::parse(big_text);
CHECK(d.root().dump() == big_text);
CHECK(d.root().dump(4) == big.dump(4));
}
SECTION("editable document")
{
alt_editable_document d = alt_editable_document::parse(text);
d.set(d.root(), "f", "new");
d.push_back(d.root()[alt_string("a")], 3);
alt_json expected = j;
expected["f"] = "new";
expected["a"].push_back(3);
CHECK(d.root().materialize() == expected);
CHECK(alt_json::parse(d.root().dump()) == expected);
const alt_editable_document loaded = alt_editable_document::load(d.save());
CHECK(loaded.root().materialize() == expected);
}
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP DOCTEST_CLANG_SUPPRESS_WARNING_POP
+68
View File
@@ -14,7 +14,9 @@ using nlohmann::json;
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <memory>
#include <sstream> #include <sstream>
#include <stdexcept>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -767,3 +769,69 @@ TEST_CASE("a write that throws leaves the same bytes in every kind of output")
} }
} }
#endif #endif
#if !defined(JSON_NOEXCEPTION)
namespace
{
// an adapter whose every write fails
class failing_adapter : public nlohmann::detail::output_adapter_protocol<char>
{
public:
[[noreturn]] void write_character(char /*c*/) override
{
throw std::runtime_error("write failed");
}
[[noreturn]] void write_characters(const char* /*s*/, std::size_t /*length*/) override
{
throw std::runtime_error("write failed");
}
};
} // namespace
#endif
TEST_CASE("output adapters")
{
SECTION("writing a single character")
{
// the writers pass whole buffers on, but an adapter takes single
// characters as well
std::vector<char> as_vector;
nlohmann::detail::output_vector_adapter<char> vector_adapter(as_vector);
vector_adapter.write_character('a');
vector_adapter.write_character('b');
CHECK(as_vector == std::vector<char>({'a', 'b'}));
std::ostringstream as_stream;
nlohmann::detail::output_stream_adapter<char> stream_adapter(as_stream);
stream_adapter.write_character('c');
stream_adapter.write_character('d');
CHECK(as_stream.str() == "cd");
std::string as_string;
nlohmann::detail::output_string_adapter<char> string_adapter(as_string);
string_adapter.write_character('e');
string_adapter.write_character('f');
CHECK(as_string == "ef");
}
#if !defined(JSON_NOEXCEPTION)
SECTION("a sink that is destroyed with characters left over")
{
// the destructor passes the pending characters on, as a write that
// threw leaves them there; the adapter failing then must not escape
{
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
sink.write_character('x');
}
// while flush() reports the failure
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
sink.write_character('x');
CHECK_THROWS_AS(sink.flush(), std::runtime_error&);
// (the character counts as written, so there is nothing left for the destructor)
}
#endif
}
+35 -7
View File
@@ -78,23 +78,25 @@ TEST_CASE("BJData")
{ {
SECTION("binary_reader BJData lookup tables") SECTION("binary_reader BJData lookup tables")
{ {
// both lookups are static member functions // bjd_type_name() is static; is_valid_optimized_type() needs a BJData reader
std::vector<std::uint8_t> const data; std::vector<std::uint8_t> const data;
using reader_t = nlohmann::detail::binary_reader<json, decltype(nlohmann::detail::input_adapter(data))>; using reader_t = nlohmann::detail::binary_reader<json, decltype(nlohmann::detail::input_adapter(data))>;
// the excluded optimized-type markers must match binary_writer's // the markers BJData excludes as optimized types must match
// is_bjdata_excluded_type_marker(), which encodes the same 8 markers // binary_writer's is_bjdata_excluded_type_marker(), which encodes the
// same 8 markers
reader_t reader(nlohmann::detail::input_adapter(data), json::input_format_t::bjdata);
for (const char marker : for (const char marker :
{'[', '{', 'S', 'H', 'T', 'F', 'N', 'Z' {'[', '{', 'S', 'H', 'T', 'F', 'N', 'Z', 'x'
}) })
{ {
CHECK(reader_t::is_bjd_excluded_optimized_type(static_cast<unsigned char>(marker))); CHECK(!reader.is_valid_optimized_type(static_cast<unsigned char>(marker)));
} }
for (const char marker : for (const char marker :
{'U', 'i', 'u', 'I', 'm', 'l', 'M', 'L', 'd', 'D', 'C', 'B', 'x' {'U', 'i', 'u', 'I', 'm', 'l', 'M', 'L', 'd', 'D', 'C', 'B', 'h'
}) })
{ {
CHECK(!reader_t::is_bjd_excluded_optimized_type(static_cast<unsigned char>(marker))); CHECK(reader.is_valid_optimized_type(static_cast<unsigned char>(marker)));
} }
// every dtype marker must round-trip to its ND-array type name // every dtype marker must round-trip to its ND-array type name
@@ -2312,6 +2314,23 @@ TEST_CASE("BJData")
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata)); CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
} }
SECTION("every event of an ndarray of chars")
{
// the elements of an ndarray of 'C' are read one by one
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 'd'};
// start_object, key, string, key, start_array, 2 numbers, end_array,
// key, start_array, 4 chars, end_array, end_object
constexpr int events = 16;
for (int allowed = 0; allowed < events; ++allowed)
{
CAPTURE(allowed)
SaxCountdown scp(allowed);
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
}
SaxCountdown scp(events);
CHECK(json::sax_parse(v, &scp, json::input_format_t::bjdata));
}
SECTION("start_array() in ndarray _ArrayData_") SECTION("start_array() in ndarray _ArrayData_")
{ {
std::vector<uint8_t> const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1}; std::vector<uint8_t> const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1};
@@ -3095,6 +3114,15 @@ TEST_CASE("BJData")
json _; json _;
CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82"); CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82");
} }
SECTION("ndarray of chars cut off by the end of the input")
{
// 2x2 chars announced, three present
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c'};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.110] parse error at byte 16: syntax error while parsing BJData char: unexpected end of input", json::parse_error&);
CHECK(json::from_bjdata(v, true, false).is_discarded());
}
} }
SECTION("byte") SECTION("byte")
+3
View File
@@ -329,6 +329,9 @@ TEST_CASE("BON8")
check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9}); check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9});
check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD}); check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD});
check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE}); check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE});
// an array that starts with a number: -1, 0, and 1 have bytes of their own
check_bon8({-1.0, 0.0, 1.0}, {0x83, 0xFB, 0xFC, 0xFD});
check_bon8({1.0, -1.0, -0.0}, {0x83, 0xFD, 0xFB, 0x8E, 0x80, 0x00, 0x00, 0x00});
check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91}); check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91});
check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF}); check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF});
check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF}); check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF});
+75
View File
@@ -82,8 +82,20 @@ class huge_string_t : public std::string
return result; return result;
} }
// returns a copy of @a s whose size() pretends to be @a n
static huge_string_t as_size(const std::string& s, const size_type n)
{
huge_string_t result(s);
result.fake_size = n;
return result;
}
size_type size() const noexcept size_type size() const noexcept
{ {
if (fake_size != 0)
{
return fake_size;
}
if (pretend_huge) if (pretend_huge)
{ {
// one byte more than the BSON length field can represent // one byte more than the BSON length field can represent
@@ -94,6 +106,7 @@ class huge_string_t : public std::string
private: private:
bool pretend_huge = false; bool pretend_huge = false;
size_type fake_size = 0;
}; };
using huge_string_json = nlohmann::json::with_string_t<huge_string_t>; using huge_string_json = nlohmann::json::with_string_t<huge_string_t>;
@@ -268,6 +281,17 @@ TEST_CASE("BSON")
CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&); CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&);
} }
SECTION("an array element that no longer fits the document")
{
// the string fits the length field, but not the document that
// holds it; nothing of it is written
const auto limit = static_cast<std::string::size_type>((std::numeric_limits<std::int32_t>::max)());
huge_string_json j;
j["a"] = huge_string_json::array({huge_string_t::as_size("value", limit - 8)});
CHECK_THROWS_AS(huge_string_json::to_bson(j), huge_string_json::out_of_range&);
}
SECTION("document") SECTION("document")
{ {
// an oversized string nested one level deep makes the // an oversized string nested one level deep makes the
@@ -899,6 +923,38 @@ TEST_CASE("BSON")
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&); CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
} }
SECTION("binary member with subtype 0x02 (old binary) with a truncated inner length")
{
// the inner length field is cut off by the end of the input
std::vector<std::uint8_t> const input =
{
0x0F, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'a', 0x00, // entry: binary
0x08, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x01 // first byte of the inner length
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON number: unexpected end of input", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("string member that is cut off by the end of the input")
{
std::vector<std::uint8_t> const input =
{
0x0F, 0x00, 0x00, 0x00, // size (little endian)
0x02, 'a', 0x00, // entry: string
0x0A, 0x00, 0x00, 0x00, // size of the string (little endian)
'b', 'c' // the string ends early
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON string: unexpected end of input", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("Some more complex document") SECTION("Some more complex document")
{ {
json const j = json const j =
@@ -2174,6 +2230,25 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
CHECK(output.empty()); CHECK(output.empty());
} }
#if !defined(JSON_NOEXCEPTION)
SECTION("U+0000 in a long key is found at any position")
{
// the key is searched eight characters at a time
for (const std::string& key :
{
std::string("abc\0defghijkl", 12), std::string("abcdefgh\0ijkl", 13), std::string("abcdefghijklmnop\0", 17), std::string("\0abcdefghijkl", 13),
std::string("abcdefghijklmno\0", 16)
})
{
CAPTURE(key)
const json value = json::object({{key, 1}});
std::vector<std::uint8_t> output;
CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&);
CHECK(output.empty());
}
}
#endif
SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)") SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)")
{ {
// the offending value is nested, so this also covers that the check // the offending value is nested, so this also covers that the check
+52
View File
@@ -1809,6 +1809,30 @@ TEST_CASE("CBOR")
CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded());
} }
SECTION("tag in front of a key that is cut off by the end of the input")
{
// 0xD8: the tag number is in the next byte, which is missing
const std::vector<std::uint8_t> input = {0xA1, 0xD8};
json _;
for (const auto handler :
{
json::cbor_tag_handler_t::ignore, json::cbor_tag_handler_t::store
})
{
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input, true, true, handler), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false, handler).is_discarded());
}
}
SECTION("chunk of an indefinite-length string that is cut off by the end of the input")
{
// the second byte announces a chunk of three bytes, but two follow
const std::vector<std::uint8_t> input = {0x7F, 0x63, 'a', 'b'};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false).is_discarded());
}
SECTION("tag in front of a key") SECTION("tag in front of a key")
{ {
const auto ignore = json::cbor_tag_handler_t::ignore; const auto ignore = json::cbor_tag_handler_t::ignore;
@@ -2520,6 +2544,34 @@ TEST_CASE("CBOR arrays of scalars from contiguous and non-contiguous input")
} }
} }
TEST_CASE("CBOR arrays of one element from contiguous and non-contiguous input")
{
// contiguous input leaves an array of one element to the general path
// rather than to the loop that reads arrays of scalars; both inputs must
// still give the same events, value, or error for the complete document
// and for every truncation of it, with enough bytes after each array that
// the loop could have been entered
const json j = json::parse(R"({"a": [[[[1.5]]]], "b": [7], "c": [{"x": 2.5}], "d": ["s"], "e": [[1, 2]], "f": [null], "g": 1.25})");
const std::vector<std::uint8_t> bytes = json::to_cbor(j);
CHECK(json::from_cbor(bytes) == j);
for (std::size_t length = 0; length <= bytes.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_cbor(contiguous, true, false).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false).dump());
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false));
CHECK(from_contiguous.events == from_bytewise.events);
}
}
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk") TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
{ {
// Reading an indefinite-length string or byte array used to call itself // Reading an indefinite-length string or byte array used to call itself
+75
View File
@@ -2092,6 +2092,12 @@ TEST_CASE("UTF-8 validators accept the same sequences")
} }
}; };
// an ASCII byte is a sequence of its own
for (unsigned lead = 0; lead < 0x80; ++lead)
{
CHECK(nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead)).length == 1);
}
for (unsigned lead = 0x80; lead <= 0xFF; ++lead) for (unsigned lead = 0x80; lead <= 0xFF; ++lead)
{ {
const auto shape = nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead)); const auto shape = nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead));
@@ -2157,3 +2163,72 @@ TEST_CASE("UTF-8 validators accept the same sequences")
} }
} }
} }
TEST_CASE("float_bigint")
{
// the big integer of the exact comparison of a float token with the
// midpoint between two floats (digit_comparison())
using nlohmann::detail::float_bigint;
SECTION("zero")
{
// shifting zero keeps it zero, whatever the distance
float_bigint zero(0);
zero.shift_left(0);
zero.shift_left(5);
zero.shift_left(64);
CHECK(zero.compare(float_bigint(0)) == 0);
CHECK(zero.compare(float_bigint(1)) == -1);
CHECK(float_bigint(1).compare(zero) == 1);
}
SECTION("compare numbers of different lengths")
{
// the number with more 32-bit limbs is the greater one
const float_bigint small(0xFFFFFFFFu);
const float_bigint large(std::uint64_t{1} << 32u);
CHECK(small.compare(large) == -1);
CHECK(large.compare(small) == 1);
CHECK(large.compare(large) == 0);
// a shift can add a limb
float_bigint shifted(1);
shifted.shift_left(32);
CHECK(shifted.compare(large) == 0);
shifted.shift_left(1);
CHECK(shifted.compare(large) == 1);
CHECK(large.compare(shifted) == -1);
}
SECTION("compare numbers of the same length")
{
CHECK(float_bigint(2).compare(float_bigint(3)) == -1);
CHECK(float_bigint(3).compare(float_bigint(2)) == 1);
CHECK(float_bigint(3).compare(float_bigint(3)) == 0);
CHECK(float_bigint((std::uint64_t{3} << 32u) | 1u).compare(float_bigint((std::uint64_t{3} << 32u) | 2u)) == -1);
}
}
TEST_CASE("serialization of short strings")
{
// the serializer takes strings of up to 32 characters that need no
// escaping by a table lookup of its own where the scan is not vectorized
// (this file is built that way, too, with JSON_NO_SIMD)
for (const std::string& s :
{
std::string(), std::string("a"), std::string("hello"), std::string("0123456789abcdef"), std::string("0123456789abcdef0123456789abcdef"),
std::string("0123456789abcdef0123456789abcdef0")
})
{
CAPTURE(s)
const json j = s;
CHECK(j.dump() == "\"" + s + "\"");
CHECK(j.dump(-1, ' ', true) == "\"" + s + "\"");
CHECK(json({{s, s}}).dump() == "{\"" + s + "\":\"" + s + "\"}");
}
// a character that needs an escape
CHECK(json("a\"b").dump() == "\"a\\\"b\"");
CHECK(json("caf\xC3\xA9").dump() == "\"caf\xC3\xA9\"");
CHECK(json("caf\xC3\xA9").dump(-1, ' ', true) == "\"caf\\u00e9\"");
}
-645
View File
@@ -2953,651 +2953,6 @@ TEST_CASE("diagnostic positions: value lifetime, input adapters, and SAX")
} }
#endif #endif
namespace
{
/// builds a value like json::parse(), but asks the parser to recover from
/// errors (see #3989), and checks that the events it receives are balanced
class RecoveringDomParser
{
public:
explicit RecoveringDomParser(json& j, std::size_t max_errors_ = static_cast<std::size_t>(-1))
: dom(j, false)
, max_errors(max_errors_)
{}
bool null()
{
value();
return dom.null();
}
bool boolean(bool val)
{
value();
return dom.boolean(val);
}
bool number_integer(json::number_integer_t val)
{
value();
return dom.number_integer(val);
}
bool number_unsigned(json::number_unsigned_t val)
{
value();
return dom.number_unsigned(val);
}
bool number_float(json::number_float_t val, const std::string& s)
{
value();
return dom.number_float(val, s);
}
bool string(std::string& val)
{
value();
return dom.string(val);
}
bool binary(json::binary_t& val)
{
value();
return dom.binary(val);
}
bool start_object(std::size_t elements)
{
value();
stack.push_back('o');
return dom.start_object(elements);
}
bool key(std::string& val)
{
++events;
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.back() = 'v';
return dom.key(val);
}
bool end_object()
{
++events;
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_object();
}
bool start_array(std::size_t elements)
{
value();
stack.push_back('a');
return dom.start_array(elements);
}
bool end_array()
{
++events;
if (stack.empty() || stack.back() != 'a')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_array();
}
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& ex)
{
errors.emplace_back(ex.what());
return errors.size() < max_errors;
}
/// whether the events were balanced and every key was followed by a value
bool balanced() const
{
return well_formed && stack.empty();
}
/// builds the value
nlohmann::detail::json_sax_dom_parser<json> dom;
std::vector<std::string> errors {}; // NOLINT(readability-redundant-member-init)
std::size_t events = 0;
/// the open containers: 'a' for an array, 'o' for an object that expects
/// a key, 'v' for an object that expects the value of a key
std::vector<char> stack {}; // NOLINT(readability-redundant-member-init)
bool well_formed = true;
std::size_t max_errors;
private:
/// a value is passed: it is an array element, or the value of a key
void value()
{
++events;
if (!stack.empty())
{
if (stack.back() == 'v')
{
stack.back() = 'o';
}
else if (stack.back() == 'o')
{
// a value without a key
well_formed = false;
}
}
}
};
struct RecoveryResult
{
json value;
std::vector<std::string> errors;
std::size_t events;
bool ok;
bool balanced;
};
template<typename InputType>
RecoveryResult parse_recovering(InputType&& input, const bool strict = true,
const bool ignore_comments = false, const bool ignore_trailing_commas = false)
{
json j;
RecoveringDomParser sax(j);
const bool ok = json::sax_parse(std::forward<InputType>(input), &sax, json::input_format_t::json,
strict, ignore_comments, ignore_trailing_commas);
return {j, sax.errors, sax.events, ok, sax.balanced()};
}
/// stops after a number of events, but recovers from errors
class RecoveringCountdown
{
public:
explicit RecoveringCountdown(const int count)
: countdown(count)
{}
bool null()
{
return countdown.null();
}
bool boolean(bool val)
{
return countdown.boolean(val);
}
bool number_integer(json::number_integer_t val)
{
return countdown.number_integer(val);
}
bool number_unsigned(json::number_unsigned_t val)
{
return countdown.number_unsigned(val);
}
bool number_float(json::number_float_t val, const std::string& s)
{
return countdown.number_float(val, s);
}
bool string(std::string& val)
{
return countdown.string(val);
}
bool binary(json::binary_t& val)
{
return countdown.binary(val);
}
bool start_object(std::size_t elements)
{
return countdown.start_object(elements);
}
bool key(std::string& val)
{
return countdown.key(val);
}
bool end_object()
{
return countdown.end_object();
}
bool start_array(std::size_t elements)
{
return countdown.start_array(elements);
}
bool end_array()
{
return countdown.end_array();
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) // NOLINT(readability-convert-member-functions-to-static)
{
return true;
}
private:
SaxCountdown countdown;
};
/// a repaired input: the value it is repaired to, and the number of errors
struct Repair
{
const char* input;
const char* expected;
std::size_t errors;
};
} // namespace
TEST_CASE("parser error recovery (#3989)")
{
SECTION("repairs")
{
const std::vector<Repair> repairs =
{
// a missing separator is inserted
{"[1 2]", "[1,2]", 1},
{R"({"a":1 "b":2})", R"({"a":1,"b":2})", 1},
{R"({"a" 1})", R"({"a":1})", 1},
{"[1 tru 2]", "[1,null,2]", 2},
{R"({"a" "b": 1})", R"({"a":"b"})", 2},
// a missing value is null in an object; in an array, a ',' stands
// for null, while an array that ends there just ends
{R"({"a":})", R"({"a":null})", 1},
{R"({"a"})", R"({"a":null})", 1},
{R"({"a","b":1})", R"({"a":null,"b":1})", 1},
{"[1,,2]", "[1,null,2]", 1},
{"[,1]", "[null,1]", 1},
{"[1,]", "[1]", 1},
{"[1,2,3,]", "[1,2,3]", 1},
{R"({"a":1,})", R"({"a":1})", 1},
// a broken string keeps what can be read
{R"(["a\qb"])", R"(["aqb"])", 1},
{R"({"na\me":1})", R"({"name":1})", 1},
{"[\"\xFF\"]", R"(["\uFFFD"])", 1},
{"[\"a\xC3(\"]", R"(["a\uFFFD("])", 1},
{"[\"\xE2\x82\"]", R"(["\uFFFD"])", 1},
{"[\"\xC3\\\\\", 1]", R"(["\uFFFD\\",1])", 1},
{R"(["\u12"])", R"(["\uFFFD"])", 1},
{R"(["\u12G4"])", R"(["\uFFFDG4"])", 1},
{R"(["\uDC00x"])", R"(["\uFFFDx"])", 1},
{R"(["\uD800x"])", R"(["\uFFFDx"])", 1},
{R"(["\uD800\u0041"])", R"(["\uFFFDA"])", 1},
{R"(["\uD800\uD800\uDC00"])", R"(["\uFFFD\uD800\uDC00"])", 1},
{R"(["\uD800\uD800\uD800x"])", R"(["\uFFFD\uFFFD\uFFFDx"])", 1},
{
R"(["\uD800\"x", 1])", R"(["\uFFFD\"x",1])", 1
},
{R"(["\uD800\q"])", R"(["\uFFFDq"])", 1},
{"[\"a\tb\"]", R"(["a\tb"])", 1},
{R"(["a\qb\u0041\x"])", R"(["aqbAx"])", 1},
// a broken number keeps its longest valid prefix
{"[1.]", "[1]", 1},
{"[-2.]", "[-2]", 1},
{"[1.5e]", "[1.5]", 1},
{"[1e+]", "[1]", 1},
{"[1.x2, 3]", "[1,3]", 1},
// what cannot be read at all is null
{"[1,NaN,3]", "[1,null,3]", 1},
{"[tru]", "[null]", 1},
{"[-]", "[null]", 1},
{R"({"a":Infinity})", R"({"a":null})", 1},
// a stray token is dropped
{"[:1]", "[1]", 1},
{R"(["a":1])", R"(["a",1])", 1},
{R"({"a"::1})", R"({"a":1})", 1},
// a member that cannot be read is skipped
{R"({1:2,"b":3})", R"({"b":3})", 1},
{R"({"a":1 2})", R"({"a":1})", 1},
{R"({,"a":1})", R"({"a":1})", 1},
{R"({"a":1,,"b":2})", R"({"a":1,"b":2})", 1},
{"{a:1}", "{}", 1},
{R"({"a":1 [1,{"b":2}], "c":3})", R"({"a":1,"c":3})", 1},
{R"([{1}, "a"])", R"([{},"a"])", 1},
// a wrong closing bracket closes the innermost container
{R"({"a":[1,2}, "b":3})", R"({"a":[1,2],"b":3})", 1},
{R"([{"a":1], 2])", R"([{"a":1},2])", 1},
{"{]", "{}", 1},
{"[}", "[]", 1},
// the end of the input closes all containers
{R"({"a":[1,2)", R"({"a":[1,2]})", 1},
{"[", "[]", 1},
{"{", "{}", 1},
{R"({"a")", R"({"a":null})", 1},
{R"({"a":)", R"({"a":null})", 1},
{"[1,", "[1]", 1},
{"[[[1", "[[[1]]]", 1},
{
R"(["abc)", R"(["abc"])", 2
},
{"[1,tr", "[1,null]", 2},
{"\"abc", "\"abc\"", 1},
{"[\"ab\ncd\"]", R"(["ab",null,"]"])", 4},
// what comes before the top-level value is skipped
{")]}'\n{\"a\":1}", R"({"a":1})", 1},
{R"(data: {"a":1})", R"({"a":1})", 1},
{"\xEF\xBB[1]", "[1]", 1},
// what comes after it is an error that ends parsing
{R"({"a":1}})", R"({"a":1})", 1},
{"[1}]", "[1]", 2},
{"[1] [2]", "[1]", 1},
};
for (const auto& repair : repairs)
{
CAPTURE(repair.input)
const auto result = parse_recovering(std::string(repair.input));
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.value == json::parse(repair.expected));
CHECK(result.errors.size() == repair.errors);
}
}
SECTION("number overflow")
{
const auto result = parse_recovering(std::string("[1e999,-1e999]"));
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.errors.size() == 2);
CHECK(result.errors[0] == "[json.exception.out_of_range.406] number overflow parsing '1e999'");
REQUIRE(result.value.size() == 2);
CHECK(result.value[0].is_number_float());
CHECK(result.value[0].get<double>() == std::numeric_limits<double>::infinity());
CHECK(result.value[1].get<double>() == -std::numeric_limits<double>::infinity());
// the SAX parser gets the number's text
SaxEventLogger logger;
logger.recover = true;
CHECK(!json::sax_parse("1e999", &logger));
CHECK(logger.events == std::vector<std::string>({"parse_error(5)", "number_float(1e999)"}));
}
SECTION("nothing to recover")
{
for (const std::string s :
{
"", " ", "]", "tru", "NaN", ",:", "/* comment"
})
{
CAPTURE(s)
const auto result = parse_recovering(s, true, true);
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.events == 0);
CHECK(result.value == nullptr);
CHECK(result.errors.size() == 1);
}
}
SECTION("error messages")
{
// the first error is reported as without recovery
for (const std::string s :
{
"[1 2]", R"({"a":1 "b":2})", R"({"a" 1})", R"({"a":})", "[1,]", "[1.]",
R"(["a\qb"])", "[1e999]", "{1:2}", R"({"a":[1,2}})", "[1,", "[1] [2]", "{a:1}"
})
{
CAPTURE(s)
const auto result = parse_recovering(s);
REQUIRE(!result.errors.empty());
json _;
CHECK_THROWS_WITH_STD_STR(_ = json::parse(s), result.errors.front());
}
// the token of an error begins where the previous error was
const auto result = parse_recovering(std::string("[tru, fals, nul]"));
CHECK(result.errors == std::vector<std::string>(
{
"[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - invalid literal; last read: '[tru,'",
"[json.exception.parse_error.101] parse error at line 1, column 11: syntax error while parsing value - invalid literal; last read: ', fals,'",
"[json.exception.parse_error.101] parse error at line 1, column 16: syntax error while parsing value - invalid literal; last read: ', nul]'"
}));
CHECK(result.value == json::parse("[null,null,null]"));
}
SECTION("events")
{
// see #4522
SaxEventLogger logger;
logger.recover = true;
CHECK(!json::sax_parse(R"([{1}, "a"])", &logger));
CHECK(logger.events == std::vector<std::string>(
{
"start_array()", "start_object()", "parse_error(3)", "end_object()", "string(a)", "end_array()"
}));
}
SECTION("options")
{
SECTION("strict")
{
const auto result = parse_recovering(std::string("[1 2] [3]"), false);
CHECK(!result.ok);
CHECK(result.value == json::parse("[1,2]"));
CHECK(result.errors.size() == 1);
}
SECTION("ignore_trailing_commas")
{
for (const std::string s :
{
"[1,]", R"({"a":1,})", "[[1,],]"
})
{
CAPTURE(s)
const auto result = parse_recovering(s, true, false, true);
CHECK(result.ok);
CHECK(result.errors.empty());
}
auto result = parse_recovering(std::string("[1,,]"), true, false, true);
CHECK(result.value == json::parse("[1,null]"));
CHECK(result.errors.size() == 1);
result = parse_recovering(std::string(R"({"a":1,,})"), true, false, true);
CHECK(result.value == json::parse(R"({"a":1})"));
CHECK(result.errors.size() == 1);
}
SECTION("ignore_comments")
{
auto result = parse_recovering(std::string("[1 /* one */ 2]"), true, true);
CHECK(result.value == json::parse("[1,2]"));
CHECK(result.errors.size() == 1);
// a comment that is not closed runs to the end of the input, which
// is not reported again
result = parse_recovering(std::string("[1, 2 /* unterminated"), true, true);
CHECK(result.balanced);
CHECK(result.value == json::parse("[1,2]"));
CHECK(result.errors.size() == 1);
// a '/' that does not begin a comment is garbage
result = parse_recovering(std::string("[1, /x, 2]"), true, true);
CHECK(result.balanced);
CHECK(result.value == json::parse("[1,null,2]"));
CHECK(result.errors.size() == 1);
}
}
SECTION("null bytes")
{
// a null byte ends the input, unless JSON_STRICT_NUL_HANDLING is set
const auto result = parse_recovering(std::string("[1,\0x", 5));
CHECK(result.balanced);
CHECK(!result.ok);
#ifdef JSON_TEST_STRICT_NUL_HANDLING_ENABLED
CHECK(result.value == json::parse("[1,null]"));
#else
CHECK(result.value == json::parse("[1]"));
CHECK(result.errors.size() == 1);
#endif
const auto in_string = parse_recovering(std::string("[\"a\0b\"]", 7));
CHECK(in_string.balanced);
#ifdef JSON_TEST_STRICT_NUL_HANDLING_ENABLED
CHECK(in_string.value == json::array({std::string("a\0b", 3)}));
#else
CHECK(in_string.value == json::parse(R"(["a"])"));
#endif
}
SECTION("the SAX parser stops recovering")
{
json j;
RecoveringDomParser sax(j, 2);
CHECK(!json::sax_parse("[1 2 3 4 5]", &sax));
CHECK(sax.errors.size() == 2);
// an error at a delimiter that an invalid token consumed is reported
// to the SAX parser, too
json j2;
RecoveringDomParser sax2(j2, 2);
CHECK(!json::sax_parse("[tru}, 1]", &sax2));
CHECK(sax2.errors.size() == 2);
}
SECTION("an event stops parsing during a repair")
{
// start_object() and key() are passed, then null() for the missing
// value returns false
RecoveringCountdown countdown(2);
CHECK(!json::sax_parse(R"({"a":})", &countdown));
// the end of the input: end_array() for the second array returns false
RecoveringCountdown countdown2(4);
CHECK(!json::sax_parse("[[1", &countdown2));
}
SECTION("input adapters")
{
// the lexer reads contiguous and streaming input differently, and it
// puts back a character that ended an invalid token
for (const std::string s :
{
"[1 2]", "[tru}, 1]", R"({"a" "b\q", "c":[1.x, 2}})", "[\"\xFF\xC3(\", -, 1e+]", "{a:1,\"b\":2", ")]}' [1]"
})
{
CAPTURE(s)
const auto reference = parse_recovering(s);
CHECK(reference.balanced);
const auto from_c_string = parse_recovering(s.c_str());
CHECK(from_c_string.value == reference.value);
CHECK(from_c_string.errors == reference.errors);
const std::list<char> l(s.begin(), s.end());
json j;
RecoveringDomParser sax(j);
CHECK(!json::sax_parse(l.begin(), l.end(), &sax));
CHECK(j == reference.value);
CHECK(sax.errors == reference.errors);
std::istringstream ss(s);
const auto from_stream = parse_recovering(ss);
CHECK(from_stream.value == reference.value);
CHECK(from_stream.errors == reference.errors);
}
}
SECTION("long runs of errors")
{
// no error may copy all the input read before it
const auto closing = parse_recovering("[" + std::string(100000, '}'));
CHECK(closing.balanced);
CHECK(closing.value == json::array());
const auto garbage = parse_recovering("[" + std::string(100000, 'x') + "]");
CHECK(garbage.balanced);
CHECK(garbage.errors.size() == 1);
const auto commas = parse_recovering("{" + std::string(100000, ',') + "}");
CHECK(commas.balanced);
CHECK(commas.value == json::object());
}
SECTION("mutations of valid input")
{
// whatever the input, the events are balanced, every error is reported
// at most once, and valid input is parsed as usual
const std::vector<std::string> documents =
{
R"({"name": "value", "list": [1, -2.5, true, null, {"x": [[]]}], "e": "\u00e9"})",
R"([{"a": [1, 2, {"b": "c"}]}, [], {}, "\ud83d\ude00", 1e10])",
"{\"\xC3\xA9\": \"\xF0\x9F\x98\x80\"}",
R"( {"k" : [ "v" , 0 ] } )",
};
// each character that can be inserted, including a null byte
const std::string insertions("[]{},:\"x\\\0\xFF", 11);
std::vector<std::string> inputs;
for (const auto& doc : documents)
{
for (std::size_t i = 0; i <= doc.size(); ++i)
{
inputs.push_back(doc.substr(0, i));
if (i < doc.size())
{
inputs.push_back(doc.substr(0, i) + doc.substr(i + 1));
}
for (const char c : insertions)
{
inputs.push_back(doc.substr(0, i) + c + doc.substr(i));
}
}
}
for (const auto& s : inputs)
{
CAPTURE(s)
const auto result = parse_recovering(s);
CHECK(result.balanced);
CHECK(result.errors.size() <= s.size() + 1);
CHECK(result.events <= (4 * s.size()) + 4);
if (json::accept(s))
{
CHECK(result.ok);
CHECK(result.errors.empty());
CHECK(result.value == json::parse(s));
}
else
{
CHECK(!result.ok);
CHECK(!result.errors.empty());
}
}
}
}
TEST_CASE("accept() agrees with parse() on floats around the overflow limit") TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
{ {
// accept() does not convert a float that cannot overflow; the tokens // accept() does not convert a float that cannot overflow; the tokens
+38
View File
@@ -1260,6 +1260,44 @@ TEST_CASE("value conversion")
#undef JSON_HAS_CPP_17 #undef JSON_HAS_CPP_17
#endif #endif
TEST_CASE("get<T>() of a boolean (#5988)")
{
// a boolean converts to 0/1 for every arithmetic type except number_integer_t, number_unsigned_t and
// number_float_t, whose dedicated from_json() overloads throw
const json t = true;
const json f = false;
SECTION("0 and 1")
{
// long and unsigned long are left out: they are std::int64_t and std::uint64_t on some platforms
CHECK(t.get<bool>() == true);
CHECK(f.get<bool>() == false);
CHECK(t.get<char>() == 1);
CHECK(t.get<signed char>() == 1);
CHECK(t.get<unsigned char>() == 1);
CHECK(t.get<short>() == 1); // NOLINT(google-runtime-int)
CHECK(t.get<unsigned short>() == 1); // NOLINT(google-runtime-int)
CHECK(t.get<int>() == 1);
CHECK(t.get<unsigned>() == 1);
CHECK(t.get<float>() == 1.0F);
CHECK(t.get<long double>() == 1.0L);
CHECK(f.get<int>() == 0);
CHECK(f.get<float>() == 0.0F);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("number_integer_t, number_unsigned_t and number_float_t throw")
{
CHECK_THROWS_WITH_AS(t.get<json::number_integer_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
CHECK_THROWS_WITH_AS(t.get<json::number_unsigned_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
CHECK_THROWS_WITH_AS(t.get<json::number_float_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
CHECK_THROWS_WITH_AS(f.get<json::number_integer_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
CHECK_THROWS_WITH_AS(f.get<json::number_unsigned_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
CHECK_THROWS_WITH_AS(f.get<json::number_float_t>(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&);
}
#endif
}
#ifdef JSON_HAS_CPP_14 #ifdef JSON_HAS_CPP_14
#undef JSON_HAS_CPP_14 #undef JSON_HAS_CPP_14
#endif #endif
+20
View File
@@ -210,3 +210,23 @@ TEST_CASE("regression test #5476 - array type without reserve()")
CHECK(json::from_cbor(truncated, true, false).is_discarded()); CHECK(json::from_cbor(truncated, true, false).is_discarded());
} }
} }
TEST_CASE("array type with std::deque nested beyond the recursion limit")
{
// the binary writers finish a value nested deeper than
// recursion_depth_limit() with an explicit stack of frames; with the
// iterators of std::deque, these frames are copied and moved through
// their own special members
deque_json j = 1;
for (int i = 0; i < 200; ++i)
{
deque_json outer = deque_json::array();
outer.push_back(std::move(j));
j = std::move(outer);
}
// 200 arrays of one element each (0x81) around the integer 1
std::vector<std::uint8_t> expected(200, 0x81);
expected.push_back(0x01);
CHECK(deque_json::to_cbor(j) == expected);
}
+90
View File
@@ -7,6 +7,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#include <algorithm> #include <algorithm>
#include <memory>
#include <set> #include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -495,3 +496,92 @@ TEST_CASE("Regression test for issue #5674 - copy construction must not require
CHECK(*c == 1); CHECK(*c == 1);
} }
} }
// counts the constructions and destructions of the base class (#5995)
struct counting_base
{
static std::size_t constructions;
static std::size_t destructions;
counting_base() noexcept
{
++constructions;
}
counting_base(const counting_base& /*unused*/) noexcept
{
++constructions;
}
counting_base(counting_base&& /*unused*/) noexcept
{
++constructions;
}
counting_base& operator=(const counting_base& /*unused*/) noexcept = default;
counting_base& operator=(counting_base&& /*unused*/) noexcept = default;
~counting_base()
{
++destructions;
}
static void reset() noexcept
{
constructions = 0;
destructions = 0;
}
};
std::size_t counting_base::constructions = 0;
std::size_t counting_base::destructions = 0;
using json_with_counting_base = nlohmann::json::with_base_class_t<counting_base>;
TEST_CASE("destroying a value does not construct the custom base class")
{
// regression test for #5995: destroying an array or object used a
// default-constructed basic_json as a stand-in, which constructed a base
// class that belonged to no value (and could allocate in the noexcept
// destructor); now every destroyed value destroys exactly its own base
// object, and none is constructed
const auto check_destroy = [](std::unique_ptr<json_with_counting_base> j, std::size_t values)
{
counting_base::reset();
j = nullptr;
CHECK(counting_base::constructions == 0);
CHECK(counting_base::destructions == values);
};
SECTION("scalar")
{
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(42)), 1);
}
SECTION("array and object")
{
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(json_with_counting_base::parse(R"({"a":[1,2,3],"b":{"c":true}})"))), 7);
check_destroy(std::unique_ptr<json_with_counting_base>(new json_with_counting_base(json_with_counting_base::array())), 1);
}
SECTION("nested deeper than the recursion depth limit")
{
// alternating arrays and objects, so both kinds of container are
// walked by destroy_walk()
constexpr std::size_t depth = 1000;
std::unique_ptr<json_with_counting_base> j(new json_with_counting_base(json_with_counting_base::array({1, 2})));
for (std::size_t i = 0; i < depth; ++i)
{
json_with_counting_base outer = (i % 2 == 0) ? json_with_counting_base::object() : json_with_counting_base::array();
if (outer.is_object())
{
outer["x"] = std::move(*j);
outer["y"] = nullptr;
}
else
{
outer.push_back(std::move(*j));
outer.push_back(true);
}
*j = std::move(outer);
}
// the innermost array with its two elements, and two values per level
check_destroy(std::move(j), 3 + (2 * depth));
}
}
+21
View File
@@ -49,3 +49,24 @@ TEST_CASE("custom object key types: MessagePack (json_full)")
{ {
custom_key_test::test_msgpack<json_full>(); custom_key_test::test_msgpack<json_full>();
} }
TEST_CASE("custom object key types: patch (json_full)")
{
// the members of a patch are found by lookup, as their keys are not std::string
const json_full j = json_full::parse(R"({"a":1,"b":[1,2],"c":{"d":2}})");
const json_full patch = json_full::parse(R"([
{"op": "add", "path": "/e", "value": 5},
{"op": "remove", "path": "/a"},
{"op": "replace", "path": "/c/d", "value": 3},
{"op": "test", "path": "/c/d", "value": 3},
{"op": "move", "from": "/e", "path": "/f"},
{"op": "copy", "from": "/f", "path": "/g"},
{"op": "add", "path": "/b/-", "value": 7}
])");
CHECK(j.patch(patch).dump() == R"({"b":[1,2,7],"c":{"d":3},"f":5,"g":5})");
// a patch with a missing member is rejected
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "add", "path": "/e"}])")), json_full::parse_error&);
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "test", "path": "/a", "value": 2}])")), json_full::other_error&);
}
+66
View File
@@ -337,6 +337,52 @@ TEST_CASE("custom number types: integers beyond 64 bits")
CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64)); CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64));
} }
SECTION("MessagePack integers of every width")
{
// an integer of a number type wider than 64 bits is written with the
// smallest MessagePack integer that holds it, as for 64-bit types
const wide_json widths = wide_json::array(
{
make_int(-33), make_int(-100), make_int(-128), make_int(-129), make_int(-32768), make_int(-32769), make_int(-2147483648LL), make_int(-2147483649LL),
make_int(128), make_int(255), make_int(256), make_int(300), make_int(65535), make_int(65536), make_int(70000), make_int(4294967295LL), make_int(4294967296LL),
make_uint(128), make_uint(255), make_uint(256), make_uint(300), make_uint(65535), make_uint(65536), make_uint(70000), make_uint(4294967295ULL), make_uint(4294967296ULL)
});
const nlohmann::json widths64 = nlohmann::json::array(
{
-33, -100, -128, -129, -32768, -32769, -2147483648LL, -2147483649LL,
128, 255, 256, 300, 65535, 65536, 70000, 4294967295LL, 4294967296LL,
128u, 255u, 256u, 300u, 65535u, 65536u, 70000u, 4294967295ULL, 4294967296ULL
});
CHECK(wide_json::to_msgpack(widths) == nlohmann::json::to_msgpack(widths64));
// as single values, too
CHECK(wide_json::to_msgpack(make_int(-100)) == std::vector<std::uint8_t>({0xD0, 0x9C}));
CHECK(wide_json::to_msgpack(make_int(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
CHECK(wide_json::to_msgpack(make_int(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
CHECK(wide_json::to_msgpack(make_uint(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
CHECK(wide_json::to_msgpack(make_uint(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
}
SECTION("dump")
{
// beyond 64 bits, the digits are counted before they are written
CHECK(make_int(0).dump() == "0");
CHECK(make_int(two_64).dump() == "18446744073709551616");
CHECK(make_int(-two_64).dump() == "-18446744073709551616");
CHECK(make_int(two_100).dump() == "1267650600228229401496703205376");
CHECK(make_int(-two_100).dump() == "-1267650600228229401496703205376");
// powers of ten, where the count of digits ends on a single digit or two
const int128 ten_20 = static_cast<int128>(100000000000ULL) * static_cast<int128>(1000000000ULL);
CHECK(make_int(ten_20).dump() == "100000000000000000000");
CHECK(make_int(-ten_20).dump() == "-100000000000000000000");
CHECK(make_int(ten_20 * 10).dump() == "1000000000000000000000");
CHECK(make_int(ten_20 * 100).dump() == "10000000000000000000000");
CHECK(make_int(ten_20 * 1000).dump() == "100000000000000000000000");
CHECK(make_uint(static_cast<uint128>(ten_20) * 10u).dump() == "1000000000000000000000");
CHECK(make_uint((std::numeric_limits<uint128>::max)()).dump() == "340282366920938463463374607431768211455");
CHECK(make_int((std::numeric_limits<int128>::min)()).dump() == "-170141183460469231731687303715884105728");
}
SECTION("BJData ND-array") SECTION("BJData ND-array")
{ {
// an ND-array element beyond 64 bits does not fit any dtype, so the // an ND-array element beyond 64 bits does not fit any dtype, so the
@@ -417,3 +463,23 @@ TEST_CASE("custom number types")
} }
} }
TEST_CASE("custom number types: 32-bit integers in the binary formats")
{
// integers narrower than 64 bits are widened (a signed one with its sign)
// before the CBOR, MessagePack, and UBJSON writers pick their encoding
using json32 = nlohmann::json::with_integers_t<std::int32_t, std::uint32_t>;
json32 values = json32::array();
values.push_back(std::uint32_t{0});
values.push_back(std::uint32_t{200});
values.push_back(std::uint32_t{4294967295u});
values.push_back(std::int32_t{-1});
values.push_back(std::int32_t{-2147483647 - 1});
REQUIRE(values[2].is_number_unsigned());
REQUIRE(values[4].is_number_integer());
const nlohmann::json values64 = {0u, 200u, 4294967295u, -1, -2147483648LL};
CHECK(json32::to_cbor(values) == nlohmann::json::to_cbor(values64));
CHECK(json32::to_msgpack(values) == nlohmann::json::to_msgpack(values64));
CHECK(json32::to_ubjson(values) == nlohmann::json::to_ubjson(values64));
}
+163
View File
@@ -11,8 +11,13 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <string> #include <string>
#include <type_traits>
#include <utility> #include <utility>
#include <vector>
#include "custom_object_key_type.hpp" #include "custom_object_key_type.hpp"
@@ -125,3 +130,161 @@ TEST_CASE("MessagePack supports custom object key types nested deeper than the r
CHECK(encoded == nlohmann::json::to_msgpack(expected)); CHECK(encoded == nlohmann::json::to_msgpack(expected));
CHECK(nlohmann::json::from_msgpack(encoded) == expected); CHECK(nlohmann::json::from_msgpack(encoded) == expected);
} }
namespace
{
// A key type that cannot be constructed from a std::string, so that a value
// with such keys cannot be converted from a nlohmann::json by the range
// constructor of its object type: the serializer converts the object instead.
class strict_key
{
public:
strict_key(const char* value)
: m_value(value)
{}
strict_key(const std::string& /*value*/) = delete;
#if JSON_DIAGNOSTICS
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
// when building the path of an exception.
const char* data() const noexcept
{
return m_value.data();
}
#endif
friend bool operator<(const strict_key& lhs, const strict_key& rhs)
{
return lhs.m_value < rhs.m_value;
}
private:
std::string m_value{};
};
strict_key make_strict_key(const std::string& key)
{
return strict_key(key.c_str());
}
template<class Key, class Value, class Compare, class Allocator>
class strict_object
: public std::map <
strict_key,
Value,
std::less<strict_key>, // NOLINT(modernize-use-transparent-functors)
typename std::allocator_traits<Allocator>::template rebind_alloc <
std::pair<const strict_key, Value >>>
{
public:
strict_object() = default;
};
template<typename T, typename = void>
struct strict_serializer : nlohmann::adl_serializer<T> {};
using strict_json = nlohmann::basic_json < strict_object, std::vector, std::string, bool, std::int64_t,
std::uint64_t, double, std::allocator, strict_serializer, std::vector<std::uint8_t >>;
// the members of an object are converted one by one, their keys by make_strict_key()
template<>
struct strict_serializer<nlohmann::json::object_t, void>
{
static void to_json(strict_json& j, const nlohmann::json::object_t& object)
{
strict_json::object_t converted;
for (const auto& member : object)
{
converted.emplace(make_strict_key(member.first), strict_json(member.second));
}
j = strict_json(std::move(converted));
}
};
} // namespace
TEST_CASE("converting a value with keys that cannot be made from a std::string nested deeper than the recursion depth limit")
{
static_assert(!std::is_constructible<strict_key, const std::string&>::value, "the keys cannot be made from a std::string");
// below detail::recursion_depth_limit(), objects and arrays are converted
// by the serializers instead of the range constructors
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10;
nlohmann::json value = 1;
for (std::size_t i = 0; i < depth; ++i)
{
if (i % 2 == 0)
{
value = nlohmann::json::object({{"a", std::move(value)}, {"b", true}});
}
else
{
value = nlohmann::json::array({std::move(value), "sibling"});
}
}
const strict_json converted = value;
// at() needs to make a std::string of a key for its error message
const auto member = [](const strict_json & object, const char* name) -> const strict_json &
{
const auto it = object.find(strict_key(name));
REQUIRE(it != object.end());
return *it;
};
// walk down the nesting levels without recursion
const strict_json* current = &converted;
for (std::size_t i = depth; i > 0; --i)
{
if (i % 2 == 1)
{
REQUIRE(current->is_object());
CHECK(current->size() == 2);
CHECK(member(*current, "b").get<bool>());
current = &member(*current, "a");
}
else
{
REQUIRE(current->is_array());
CHECK(current->size() == 2);
CHECK(current->at(1).get<std::string>() == "sibling");
current = &current->at(0);
}
}
CHECK(current->get<int>() == 1);
}
namespace
{
// shorter keys come first
struct shortlex_less
{
bool operator()(const std::string& lhs, const std::string& rhs) const
{
return lhs.size() != rhs.size() ? lhs.size() < rhs.size() : lhs < rhs;
}
};
template<class Key, class Value, class Compare, class Allocator>
using shortlex_map = std::map<Key, Value, shortlex_less, Allocator>;
using shortlex_json = nlohmann::json::with_object_t<shortlex_map>;
} // namespace
TEST_CASE("unflatten with an object type that does not sort the keys alphabetically")
{
// the keys of /a come before and after one of /b: unlike in an object
// sorted alphabetically, the values below a prefix are not found one
// after the other, so the value of /a must be looked up again
shortlex_json flat = shortlex_json::object();
flat["/a/p"] = 1;
flat["/b/qq"] = 2;
flat["/a/pqr"] = 3;
REQUIRE(flat.begin().key() == "/a/p");
REQUIRE((++flat.begin()).key() == "/b/qq");
CHECK(flat.unflatten().dump() == R"({"a":{"p":1,"pqr":3},"b":{"qq":2}})");
CHECK(flat.unflatten().flatten() == flat);
}
+22
View File
@@ -1195,6 +1195,28 @@ TEST_CASE("deserialization")
#endif #endif
} }
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
TEST_CASE("deserialization from a pointer and a length (deprecated)")
{
// the unit test build disables -Wdeprecated-declarations (see
// cmake/clang_flags.cmake)
const std::string text = R"({"foo": [1, 2, 3]})";
const std::string invalid = R"({"foo": )";
SECTION("parse")
{
const json j = json::parse({text.data(), text.size()});
CHECK(j == json({{"foo", {1, 2, 3}}}));
}
SECTION("accept")
{
CHECK(json::accept({text.data(), text.size()}));
CHECK_FALSE(json::accept({invalid.data(), invalid.size()}));
}
}
#endif
// select the types to test - char8_t is only available since C++20 if and only // select the types to test - char8_t is only available since C++20 if and only
// if __cpp_char8_t is defined. // if __cpp_char8_t is defined.
#define TYPE_LIST(...) __VA_ARGS__ #define TYPE_LIST(...) __VA_ARGS__
+33
View File
@@ -193,6 +193,28 @@ TEST_CASE("Better diagnostics with positions")
check_conversion(300); check_conversion(300);
} }
SECTION("exception about the root value (#5987)")
{
// the root has no path, but still has a byte range
const json root = json::parse("true");
const json nested = json::parse("[1, true]");
const json constructed = true;
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(root.get<std::string>(),
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
"[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean", json::type_error);
#else
CHECK_THROWS_WITH_AS(root.get<std::string>(),
"[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean", json::type_error);
CHECK_THROWS_WITH_AS(nested[1].get<std::string>(),
"[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean", json::type_error);
#endif
// a value without positions gets no range and no stray whitespace
CHECK_THROWS_WITH_AS(constructed.get<std::string>(),
"[json.exception.type_error.302] type must be string, but is boolean", json::type_error);
}
SECTION("JSON patch add to primitive parent (#4292)") SECTION("JSON patch add to primitive parent (#4292)")
{ {
// the JSON Patch "add" target /foo/bar/baz has a string parent // the JSON Patch "add" target /foo/bar/baz has a string parent
@@ -243,3 +265,14 @@ TEST_CASE("values read from a binary format have no positions")
check_no_positions(j); check_no_positions(j);
} }
} }
TEST_CASE("comparing a value-initialized iterator throws invalid_iterator.212")
{
// such an iterator has no value to take the byte positions from; the
// comparison used to dereference the null pointer instead of throwing
json j = {1, 2};
const json::iterator it{};
CHECK_THROWS_WITH_AS(it == j.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
CHECK_THROWS_WITH_AS(it < j.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
CHECK_THROWS_WITH_AS(j.begin() == it, "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", json::invalid_iterator);
}
+20
View File
@@ -868,3 +868,23 @@ TEST_CASE("Parent pointers stay consistent for object types not known to keep th
obj["1"].push_back(42); obj["1"].push_back(42);
CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&); CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&);
} }
TEST_CASE("patch_inplace() with a patch that is stored inside the patched value")
{
// the patch is modified-while-iterated if it is not copied first: it is a
// descendant of the value it is applied to, which only the parent pointers
// (JSON_DIAGNOSTICS) reveal
json j =
{
{"patch", {{{"op", "add"}, {"path", "/x"}, {"value", 1}}, {{"op", "remove"}, {"path", "/patch/0"}}}},
{"y", 2}
};
j.patch_inplace(j["patch"]);
CHECK(j.size() == 3);
CHECK(j["x"] == 1);
CHECK(j["y"] == 2);
CHECK(j["patch"].size() == 1);
CHECK(j["patch"][0]["op"] == "remove");
}
+19
View File
@@ -18,6 +18,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
#include <iterator>
#if JSON_HAS_RANGES #if JSON_HAS_RANGES
#include <algorithm> #include <algorithm>
#include <ranges> #include <ranges>
@@ -665,6 +667,23 @@ TEST_CASE("iterators 2")
} }
} }
SECTION("reverse iterators of a const value")
{
const json j_array = {1, 2, 3};
const json j_object = {{"one", 1}, {"two", 2}};
// rbegin() and rend() of a const value are crbegin() and crend()
CHECK(j_array.rbegin() == j_array.crbegin());
CHECK(j_array.rend() == j_array.crend());
CHECK(*j_array.rbegin() == 3);
CHECK(std::distance(j_array.rbegin(), j_array.rend()) == 3);
CHECK(j_object.rbegin() == j_object.crbegin());
CHECK(j_object.rend() == j_object.crend());
CHECK(j_object.rbegin().key() == "two");
CHECK(std::distance(j_object.rbegin(), j_object.rend()) == 2);
}
SECTION("reverse iterator arithmetic") SECTION("reverse iterator arithmetic")
{ {
json j_object = {{"one", 1}, {"two", 2}, {"three", 3}}; json j_object = {{"one", 1}, {"two", 2}, {"three", 3}};
+98
View File
@@ -14,9 +14,11 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif #endif
#include <cstddef>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
@@ -2453,3 +2455,99 @@ TEST_CASE("JSON patch - diff behavior documented in the API reference")
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json); CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
} }
} }
TEST_CASE("JSON patch - operations that are left to json_pointer")
{
// the usual operations on a plain pointer are applied without building a
// json_pointer; the following are not plain, and some are errors
SECTION("a path without a slash")
{
const json j = {{"a", 1}};
CHECK_THROWS_AS(j.patch(R"([{"op": "add", "path": "abc", "value": 1}])"_json), json::parse_error&);
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "abc"}])"_json), json::parse_error&);
}
SECTION("remove with an escaped reference token")
{
const json j = {{"a/b", 1}, {"c~d", 2}, {"e", 3}};
CHECK(j.patch(R"([{"op": "remove", "path": "/a~1b"}])"_json) == json({{"c~d", 2}, {"e", 3}}));
CHECK(j.patch(R"([{"op": "remove", "path": "/c~0d"}])"_json) == json({{"a/b", 1}, {"e", 3}}));
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "/missing~1key"}])"_json), json::out_of_range&);
}
SECTION("a path through a primitive value")
{
const json j = {{"a", 1}};
CHECK_THROWS_AS(j.patch(R"([{"op": "test", "path": "/a/b", "value": 1}])"_json), json::other_error&);
CHECK_THROWS_AS(j.patch(R"([{"op": "replace", "path": "/a/b", "value": 1}])"_json), json::out_of_range&);
}
}
TEST_CASE("JSON patch - diff of objects with many members")
{
// more members than diff() keeps without allocating
for (const std::size_t members :
{
16u, 17u, 40u
})
{
CAPTURE(members)
json source = json::object();
json target = json::object();
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = "key" + std::to_string(i);
source[key] = i;
// every third member differs
target[key] = (i % 3 == 0) ? i + 1 : i;
}
// the same keys, a removed and an added one
source["removed"] = true;
target["added"] = false;
const json patch = json::diff(source, target);
CHECK(source.patch(patch) == target);
CHECK(patch.size() == ((members + 2) / 3) + 2);
const nlohmann::ordered_json ordered_source = source;
const nlohmann::ordered_json ordered_target = target;
CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target);
}
}
TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive descent")
{
// the values at the innermost level are diffed without recursion: equal
// leaves yield nothing, leaves of different types and different leaves
// are replaced
const std::size_t depth = 200;
json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr};
json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr};
for (std::size_t i = 0; i < depth; ++i)
{
source = json::array({std::move(source)});
target = json::array({std::move(target)});
}
const json patch = json::diff(source, target);
CHECK(patch.size() == 3);
CHECK(source.patch(patch) == target);
// the same with objects as the outer levels
json object_source = {{"equal", 1}, {"type", "text"}, {"value", 3}};
json object_target = {{"equal", 1}, {"type", 2}, {"value", 4}};
for (std::size_t i = 0; i < depth; ++i)
{
object_source = json::object({{"a", std::move(object_source)}});
object_target = json::object({{"a", std::move(object_target)}});
}
const json object_patch = json::diff(object_source, object_target);
CHECK(object_patch.size() == 2);
CHECK(object_source.patch(object_patch) == object_target);
}
+16
View File
@@ -106,6 +106,12 @@ TEST_CASE("JSON pointers")
CHECK_THROWS_WITH_AS(p.pop_back(), CHECK_THROWS_WITH_AS(p.pop_back(),
"[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
// the top of a pointer is its first reference token
const json::json_pointer nonempty("/foo/bar/0");
CHECK(nonempty.top() == json::json_pointer("/foo"));
CHECK(nonempty.top().to_string() == "/foo");
CHECK(json::json_pointer("/a~1b").top() == json::json_pointer("/a~1b"));
SECTION("array index error") SECTION("array index error")
{ {
json v = {1, 2, 3, 4}; json v = {1, 2, 3, 4};
@@ -697,6 +703,16 @@ TEST_CASE("JSON pointers")
CHECK_THROWS_WITH_AS(j_error.unflatten(), CHECK_THROWS_WITH_AS(j_error.unflatten(),
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&); "[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
// error for an invalid escape sequence in a key
CHECK_THROWS_WITH_AS(json({{"/a~2", 1}}).unflatten(),
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
CHECK_THROWS_WITH_AS(json({{"/a/b", 1}, {"/c~", 1}}).unflatten(),
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
// error for an element of an array that is also a container
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/0/x", 2}}).unflatten(),
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
// explicit roundtrip check // explicit roundtrip check
CHECK(j.flatten().unflatten() == j); CHECK(j.flatten().unflatten() == j);
+130 -1
View File
@@ -31,9 +31,11 @@ using json_view_test::has_duplicate_keys;
#include <ios> #include <ios>
#include <istream> #include <istream>
#include <iterator> #include <iterator>
#include <functional>
#include <limits> #include <limits>
#include <list> #include <list>
#include <map> #include <map>
#include <new>
#include <random> #include <random>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -98,6 +100,37 @@ template<typename View>
using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>())); using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>()));
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
// an allocator that never has room
template<typename T>
struct failing_allocator
{
using value_type = T;
failing_allocator() = default;
template<typename U>
explicit failing_allocator(const failing_allocator<U>& /*other*/) noexcept {}
T* allocate(std::size_t /*n*/) // NOLINT(readability-convert-member-functions-to-static)
{
throw std::bad_alloc();
}
void deallocate(T* /*p*/, std::size_t /*n*/) noexcept {} // NOLINT(readability-convert-member-functions-to-static)
template<typename U>
bool operator==(const failing_allocator<U>& /*other*/) const noexcept
{
return true;
}
template<typename U>
bool operator!=(const failing_allocator<U>& /*other*/) const noexcept
{
return false;
}
};
// the exception parse() throws for a text, or "" if it accepts it // the exception parse() throws for a text, or "" if it accepts it
std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false) std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
{ {
@@ -235,7 +268,9 @@ TEST_CASE("json_view")
{ {
"", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e",
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal) "\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal)
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}" "1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}",
// (an error after every kind of value: the error is found by replaying the parser)
"[null, true, -1, 1, 1.5, \"s\", {\"a\": 1} 2]", "{\"a\": null, \"b\": -1, \"c\": 1.5, \"d\": \"s\", \"e\": {}, 1}"
}) })
{ {
CAPTURE(text) CAPTURE(text)
@@ -368,6 +403,10 @@ TEST_CASE("json_view")
CHECK(from_stream.root().materialize() == expected); CHECK(from_stream.root().materialize() == expected);
const std::list<char> list(text.begin(), text.end()); const std::list<char> list(text.begin(), text.end());
CHECK(json_document::parse(list.begin(), list.end()).owns_source()); CHECK(json_document::parse(list.begin(), list.end()).owns_source());
// (an input that is not valid)
const std::list<char> bad_list{'[', '1', ','};
json_document from_bad_list;
CHECK_THROWS_AS(from_bad_list = json_document::parse(bad_list.begin(), bad_list.end()), json::parse_error&);
CHECK(materialized<json_document>(list.begin(), list.end()) == expected); CHECK(materialized<json_document>(list.begin(), list.end()) == expected);
// iterator pairs: pointers are borrowed, and so are contiguous library // iterator pairs: pointers are borrowed, and so are contiguous library
@@ -1511,6 +1550,17 @@ TEST_CASE("json_view values")
const json_document dup = json_document::parse(R"({"a":1,"a":2})"); const json_document dup = json_document::parse(R"({"a":1,"a":2})");
CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}})); CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}}));
// a map of views from something that is not an object
using view_map = std::map<std::string, json_view>;
const json_document not_an_object = json_document::parse("[1,2]");
CHECK_THROWS_WITH_AS(not_an_object.root().get<view_map>(), json_view_test::message("type_error.302", "", 0, 5, "type must be object, but is array").c_str(), json::type_error&);
#if !defined(JSON_NOEXCEPTION)
// (and one that cannot allocate its nodes)
using failing_map = std::map<std::string, json_view, std::less<std::string>, failing_allocator<std::pair<const std::string, json_view>>>;
const json_document one_member = json_document::parse(R"({"a":1})");
CHECK_THROWS_AS(one_member.root().get<failing_map>(), std::bad_alloc&);
#endif
const json_view invalid{}; const json_view invalid{};
CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&);
CHECK(invalid.get<json>().is_discarded()); CHECK(invalid.get<json>().is_discarded());
@@ -1551,6 +1601,85 @@ TEST_CASE("json_view values")
} }
} }
namespace
{
// get<T>() of a boolean: the same value or the same exception in basic_json and in the view (#5988)
template<typename T>
void check_boolean_get_parity()
{
for (const bool b :
{
true, false
})
{
const std::string text = b ? "true" : "false";
const json j = json::parse(text);
const json_document d = json_document::parse(text);
CAPTURE(text)
#if !defined(JSON_NOEXCEPTION)
T dom_value {};
T view_value{};
const std::string dom_error = json_view_test::exception_of_call([&] { dom_value = j.get<T>(); });
const std::string view_error = json_view_test::exception_of_call([&] { view_value = d.root().template get<T>(); });
CHECK(dom_error == view_error);
if (dom_error.empty())
{
CHECK(dom_value == view_value);
CHECK(view_value == static_cast<T>(b));
}
#else
// without exceptions, the combinations that throw (number_integer_t, number_unsigned_t, number_float_t)
// cannot be run: JSON_THROW aborts
const bool throws = std::is_same<T, json::number_integer_t>::value
|| std::is_same<T, json::number_unsigned_t>::value
|| std::is_same<T, json::number_float_t>::value;
if (!throws)
{
CHECK(j.get<T>() == d.root().template get<T>());
}
#endif
}
}
} // namespace
TEST_CASE("json_view get<T>() of a boolean (#5988)")
{
check_boolean_get_parity<bool>();
check_boolean_get_parity<char>();
check_boolean_get_parity<signed char>();
check_boolean_get_parity<unsigned char>();
check_boolean_get_parity<short>(); // NOLINT(google-runtime-int)
check_boolean_get_parity<unsigned short>(); // NOLINT(google-runtime-int)
check_boolean_get_parity<int>();
check_boolean_get_parity<unsigned>();
check_boolean_get_parity<long>(); // NOLINT(google-runtime-int)
check_boolean_get_parity<unsigned long>(); // NOLINT(google-runtime-int)
check_boolean_get_parity<long long>(); // NOLINT(google-runtime-int)
check_boolean_get_parity<unsigned long long>(); // NOLINT(google-runtime-int)
check_boolean_get_parity<std::int64_t>();
check_boolean_get_parity<std::uint64_t>();
check_boolean_get_parity<float>();
check_boolean_get_parity<double>();
check_boolean_get_parity<long double>();
#if !defined(JSON_NOEXCEPTION)
SECTION("the exceptions of the exact number types")
{
const json_document d = json_document::parse("true");
CHECK_THROWS_WITH_AS(d.root().get<json::number_integer_t>(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.root().get<json::number_unsigned_t>(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.root().get<json::number_float_t>(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&);
CHECK(d.root().get<int>() == 1);
// get_to() and value() go through get<T>()
json::number_float_t x = 0;
CHECK_THROWS_AS(d.root().get_to(x), json::type_error&);
const json_document o = json_document::parse("{\"b\":true}");
CHECK_THROWS_AS(static_cast<void>(o.root().value("b", 0.0)), json::type_error&);
CHECK(o.root().value("b", 0) == 1);
}
#endif
}
TEST_CASE("json_view JSON pointers") TEST_CASE("json_view JSON pointers")
{ {
SECTION("every value of generated documents") SECTION("every value of generated documents")
@@ -0,0 +1,83 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::json_document;
#include <functional>
#include <map>
#include <string>
// These tests were split off unit-json_view_compare.cpp, whose object file got
// too large for the MinGW assembler ("file too big").
namespace
{
// an object type that does not enumerate its members in ascending order of keys
template<typename Key, typename Value, typename /*Compare*/, typename Allocator>
using descending_object = std::map<Key, Value, std::greater<Key>, Allocator>;
} // namespace
TEST_CASE("json_view comparison of differing values")
{
SECTION("values that differ in their first bytes, or are written differently")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
const bool equal = dx.root() == dy.root();
CHECK(equal == (json::parse(x) == json::parse(y)));
CHECK(equal == (dx.root() == json::parse(y)));
const nlohmann::json_editable_document ex = nlohmann::json_editable_document::parse(x);
CHECK(equal == (ex.root() == dy.root()));
return equal;
};
// booleans
CHECK(!same("true", "false"));
CHECK(!same("[true]", "[false]"));
CHECK(!same(R"({"a":false,"b":[1]})", R"({"a":true,"b":[1]})"));
CHECK(same(R"({"a":true,"b":[1]})", R"({"a":true,"b":[1]})"));
// strings and keys of equal length that differ in the first eight bytes (they are compared in words)
CHECK(!same(R"("abcdefghijklmnopqrstuvwxyz")", R"("Xbcdefghijklmnopqrstuvwxyz")"));
CHECK(!same(R"(["abcdefghijklmnopq",1])", R"(["abcdefghXjklmnopq",1])"));
CHECK(!same(R"({"abcdefghijklmnopq":1})", R"({"Xbcdefghijklmnopq":1})"));
CHECK(!same(R"({"abcdefghijklmnopq":[1]})", R"({"abcdefghijklmnopX":[1]})"));
// floats of one layout and different tokens are the same value
CHECK(same("[1.5e0, 2]", "[1.5E0, 2]"));
CHECK(same("[1.5e0, 2]", "[1.5e+0, 2]"));
CHECK(same("[1.50, 2]", "[1.5, 2]"));
CHECK(!same("[1.5e0, 2]", "[1.6e0, 2]"));
CHECK(!same("[1.5e0, 2]", "[1.5E1, 2]"));
CHECK(same(R"({"a":1.25e0,"b":[3]})", R"({"a":1.25E0,"b":[3]})"));
}
SECTION("a basic_json whose members are not sorted")
{
// (an object type that is not an ordered map, and does not keep its members sorted either)
using descending_json = nlohmann::basic_json<descending_object>;
using descending_document = nlohmann::basic_json_document<descending_json>;
std::string text = "{";
for (char key = 'a'; key <= 'p'; ++key)
{
text += (key != 'a' ? ",\"" : "\"") + std::string(1, key) + "\":[" + std::to_string(key) + "]";
}
text += "}";
const descending_json j = descending_json::parse(text);
const descending_document d = descending_document::parse(text);
CHECK(d.root() == j);
CHECK(j == d.root());
descending_json other = j;
other["k"] = 0;
CHECK(d.root() != other);
CHECK(other != d.root());
}
}
+27
View File
@@ -101,6 +101,10 @@ TEST_CASE("json_view dump")
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]"); CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]"); CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
CHECK(d.root().dump(0, ' ', true, json_view::number_format::source) == d.root().dump(0, ' ', false, json_view::number_format::source));
const json_document escaped = json_document::parse("[\"\xC3\xA9\", 1.50, {\"\xF0\x9F\x98\x80\": 1E2}]");
CHECK(escaped.root().dump(1, ' ', true, json_view::number_format::source) == "[\n \"\\u00e9\",\n 1.50,\n {\n \"\\ud83d\\ude00\": 1E2\n }\n]");
// float tokens of up to 17 significant digits in every spelling: those // float tokens of up to 17 significant digits in every spelling: those
// of at most 15 digits are written from their digits, the others // of at most 15 digits are written from their digits, the others
// through the conversion; both as dump() writes them // through the conversion; both as dump() writes them
@@ -232,6 +236,29 @@ TEST_CASE("json_view dump")
CHECK(editable.root().dump() == expected.dump()); CHECK(editable.root().dump() == expected.dump());
} }
// tokens of 16 and 17 digits with few digits after the point, and with a value of 0, which are
// converted by the general algorithm; padded, as a token near the end of the text is not read in this way
{
const std::string padding(40, ' ');
for (const char* token :
{
"123456789012.3456", "-123456789012.3456", "1234567890123.456", "12345678901234.56", "123456789012345.6", "1234567890123456.0",
"0.0000000000000000", "-0.0000000000000000", "0.000000000000000", "1234567890123.4567", "99999999999999.999"
})
{
CAPTURE(token)
const std::string padded = std::string("[") + token + padding + "]";
const json_document padded_document = json_document::parse(padded);
const json padded_json = json::parse(padded);
CHECK(padded_document.root()[0].get<double>() == padded_json[0].get<double>());
CHECK(padded_document.root().dump() == padded_json.dump());
// a float document converts the tokens as well
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
const nlohmann::basic_json_document<json_float> float_doc = nlohmann::basic_json_document<json_float>::parse(padded);
CHECK(float_doc.root()[0].get<float>() == json_float::parse(padded)[0].get<float>());
}
}
// random doubles, written as parse() and dump() would // random doubles, written as parse() and dump() would
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::string many = "["; std::string many = "[";
+40
View File
@@ -214,6 +214,24 @@ TEST_CASE("json_view dump: escaping, nesting, and output buffers")
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6)); CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
} }
SECTION("the estimate for a value that is followed by many decoded strings")
{
// (the end of the value in the source is not known when the nodes after it have decoded strings)
std::string text = "[7";
for (int i = 0; i < 20; ++i)
{
text += R"(,"\n")";
}
text += ",8]";
const json_document d = json_document::parse(text);
CHECK(d.root()[0].dump() == "7");
CHECK(d.root()[1].dump() == R"("\n")");
CHECK(d.root().dump() == json::parse(text).dump());
// the same with a container
const json_document nested = json_document::parse(R"([[7],)" + text.substr(3));
CHECK(nested.root()[0].dump() == "[7]");
}
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings") SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
{ {
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys; // plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
@@ -259,6 +277,28 @@ TEST_CASE("json_view dump: escaping, nesting, and output buffers")
} }
} }
SECTION("the length of the run that ensure_ascii copies")
{
// (dump() only asks for strings with a byte to escape, so a run that ends the string is checked here)
for (const std::size_t length :
{
std::size_t{0}, std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{31}, std::size_t{32}, std::size_t{48}, std::size_t{50}
})
{
CAPTURE(length)
// a run of printable characters, followed by a byte that is not copied and more text
std::string text(length, 'a');
text += "\"abcdefghijklmnopqrstuvwxyz";
const auto* const bytes = reinterpret_cast<const unsigned char*>(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const auto* const end = bytes + text.size();
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, end) == length);
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 1, end) == length);
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 5, end) == length);
// the end of the buffer right after the run
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, bytes + length) == length);
}
}
SECTION("ensure_ascii with DEL or a non-ASCII character at every position") SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
{ {
// the check whether a string of the source has no byte >= 0x7F reads // the check whether a string of the source has no byte >= 0x7F reads
+17
View File
@@ -547,6 +547,10 @@ TEST_CASE("json_view edits: errors")
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json(json::value_t::discarded)), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
// (an index that does not fit the document, in a null: basic_json would pad it with 4 Gi nulls)
CHECK_THROWS_AS(d.set(json::json_pointer("/z/4294967295"), 1), json::out_of_range&);
CHECK(root["z"].is_null());
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
// invalid UTF-8 is rejected when it enters the document, with the error // invalid UTF-8 is rejected when it enters the document, with the error
@@ -572,6 +576,19 @@ TEST_CASE("json_view edits: errors")
static_cast<void>(other); static_cast<void>(other);
} }
TEST_CASE("json_view edits: a container that was added is replaced by a scalar")
{
// the view of an element that push_back() added is that of the new value itself, not of
// the slot that links to it: replacing it by a scalar has to find the container it is in
json_editable_document d = json_editable_document::parse(R"({"a": [1]})");
d.push_back(d.root()["a"], json::array({2, 3}));
d.push_back(d.root()["a"], json::object({{"k", json::array({4, 5})}}));
d.set(d.root()["a"][1], 6);
d.set(d.root()["a"][2]["k"], "s");
CHECK(d.root().dump() == R"({"a":[1,6,{"k":"s"}]})");
CHECK(d.root().materialize() == json::parse(R"({"a":[1,6,{"k":"s"}]})"));
}
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: the size of a text arena") TEST_CASE("json_view edits: the size of a text arena")
{ {
+11
View File
@@ -277,6 +277,17 @@ TEST_CASE("json_view from_json")
CHECK(true); CHECK(true);
} }
SECTION("a map with enum keys from a view that is not an object")
{
// an object is read with the keys as the enum's names; any other
// value is read as an array of pairs, like a map of any other key type
const json_document d = json_document::parse(R"({"pairs": [["red", 1], ["green", 2]], "number": 1})");
const json j = d.root().materialize();
const json_view v = d.root();
CHECK(same<std::map<color, int>>(v, j, "pairs"));
CHECK((v["pairs"].get<std::map<color, int>>() == std::map<color, int> {{color::red, 1}, {color::green, 2}}));
}
SECTION("std types: the same values as from the materialized value") SECTION("std types: the same values as from the materialized value")
{ {
const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]], const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]],
+7
View File
@@ -819,6 +819,13 @@ TEST_CASE("json_view images: errors")
static_cast<void>(d); static_cast<void>(d);
}) == prefix + "too short"); }) == prefix + "too short");
// an image that is moved into the document is checked as well
CHECK(exception_of([&]
{
const json_document d = json_document::load(std::vector<std::uint8_t>(10, 0));
static_cast<void>(d);
}) == prefix + "too short");
std::vector<std::uint8_t> bad = image; std::vector<std::uint8_t> bad = image;
bad[0] = 'X'; bad[0] = 'X';
CHECK(load_result(bad, image_check::full) == prefix + "unknown format"); CHECK(load_result(bad, image_check::full) == prefix + "unknown format");
+56
View File
@@ -333,6 +333,62 @@ TEST_CASE("json_view large objects")
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t))); CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
} }
SECTION("a lookup of a missing key stops at the end of the probe window")
{
// Every slot of the first half of the table holds a key, each at its home slot, so
// that the table is built; a key that is not in the object but whose home slot is
// the first slot finds a full window of slots with other keys.
constexpr std::size_t members = 128;
constexpr std::size_t table_size = 256; // the table size for 128 members: the next power of two >= 256
for (const std::size_t prefix_length :
{
std::size_t{0}, std::size_t{20}
})
{
CAPTURE(prefix_length)
std::vector<std::string> keys(members);
std::vector<bool> taken(members, false);
std::string missing;
std::size_t found = 0;
for (std::uint64_t counter = 0; found < members || missing.empty(); ++counter)
{
std::string key(prefix_length, 'p');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key += static_cast<char>('a' + (x % 26));
}
const std::size_t slot = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (table_size - 1);
if (slot < members && !taken[slot])
{
taken[slot] = true;
keys[slot] = key;
++found;
}
else if (slot == 0 && missing.empty())
{
missing = key;
}
}
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
}
text += "}";
const json_document d = json_document::parse(text);
const json_view v = d.root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[keys[i]].get<std::size_t>() == i);
}
CHECK(!v.contains(missing));
CHECK(v.find(missing) == v.end());
CHECK(v.count(missing) == 0);
}
}
SECTION("keys that differ in their last bytes are spread over the table") SECTION("keys that differ in their last bytes are spread over the table")
{ {
// The hash must spread the usual families of keys (numbered names, hex // The hash must spread the usual families of keys (numbered names, hex
+93 -6
View File
@@ -20,6 +20,7 @@ using nlohmann::ordered_json;
using nlohmann::json_document; using nlohmann::json_document;
using nlohmann::json_editable_document; using nlohmann::json_editable_document;
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
@@ -300,6 +301,27 @@ TEST_CASE("json_view materialize() source positions")
static_cast<void>(j.at("list").at(1).get<int>()); static_cast<void>(j.at("list").at(1).get<int>());
})); }));
} }
SECTION("exception about the root value (#5987)")
{
const json_document root = json_document::parse("true");
const json_document nested = json_document::parse("[1, true]");
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(root.root().get<std::string>());
}) == "[json.exception.type_error.302] (bytes 0-4) type must be string, but is boolean");
#if JSON_DIAGNOSTICS
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(nested.root()[1].get<std::string>());
}) == "[json.exception.type_error.302] (/1) (bytes 4-8) type must be string, but is boolean");
#else
CHECK(json_view_test::exception_of_call([&]
{
static_cast<void>(nested.root()[1].get<std::string>());
}) == "[json.exception.type_error.302] (bytes 4-8) type must be string, but is boolean");
#endif
}
#endif #endif
SECTION("edited documents") SECTION("edited documents")
@@ -427,6 +449,31 @@ TEST_CASE("json_view materialize() source positions")
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2])); CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
} }
SECTION("a damaged text: the end of a container is not found")
{
// (the text of an image is not checked with image_check::none)
const std::string array_text = "[1,2]";
const std::vector<std::uint8_t> original = json_document::parse(array_text).save();
const auto at = std::search(original.begin(), original.end(), array_text.begin(), array_text.end());
REQUIRE(at != original.end());
const auto closing = static_cast<std::size_t>(at - original.begin()) + array_text.size() - 1;
// not a closing bracket, a slash that starts no comment, and the end of the text
for (const char replacement :
{
'x', '/', ' '
})
{
CAPTURE(replacement)
std::vector<std::uint8_t> damaged = original;
damaged[closing] = static_cast<std::uint8_t>(replacement);
const json_document d = json_document::load(damaged, json_document::image_check::none);
const json m = d.root().materialize();
CHECK(range(m) == "none");
CHECK(range(m[0]) == "1-2");
CHECK(range(m[1]) == "3-4");
}
}
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
SECTION("a decoded string's position must be within the text") SECTION("a decoded string's position must be within the text")
{ {
@@ -461,6 +508,7 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
{ {
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})", "null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})", R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
R"({"a\u0000b": [1, "x"], "c": {"d\u0000": 2}})", // (the path of a key ends at a null byte, as in basic_json)
R"([1 /* ] */, {"a": [] // } R"([1 /* ] */, {"a": [] // }
, "b": {} } , ])", , "b": {} } , ])",
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n", "\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
@@ -512,6 +560,49 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected); CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
} }
SECTION("views of values that are no longer part of the document, and of no value")
{
const std::string expected_string = "[json.exception.type_error.302] type must be number, but is string";
static_cast<void>(expected_string);
// (a value that is not found has no path, and with it no bytes)
{
json_editable_document d = json_editable_document::parse(R"(["x", ["y"]])");
const auto removed = d.root()[0];
d.erase(d.root(), 0);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
// (the root is a scalar now: the value cannot be in it)
d.set(d.root(), 5);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
CHECK(d.root().get<int>() == 5);
}
{
json_editable_document d = json_editable_document::parse(R"(["x"])");
const auto removed = d.root()[0];
d.set(d.root(), 5);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
}
// a view of no value has no context
CHECK(exception_of_call([&] { static_cast<void>(nlohmann::json_view().get<int>()); }) == "[json.exception.type_error.302] type must be number, but is discarded");
}
SECTION("an array whose last element was replaced by a new container has no bytes")
{
json_editable_document d = json_editable_document::parse(R"({"a": [1, [2]], "b": 1})");
d.set(d.root()["a"][1], json::array({3}));
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/a) ";
#endif
expected += "type must be number, but is array";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"].get<int>()); }) == expected);
}
SECTION("deep documents with siblings after every nested container") SECTION("deep documents with siblings after every nested container")
{ {
// the search for the path pushes and pops one frame per level (the // the search for the path pushes and pops one frame per level (the
@@ -551,12 +642,8 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value")
expected_zz += "(bytes 12-35) key 'zz' not found"; expected_zz += "(bytes 12-35) key 'zz' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz); CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json) // the root has no path, but still its bytes (#5987)
std::string expected_missing = "[json.exception.out_of_range.403] "; const std::string expected_missing = "[json.exception.out_of_range.403] (bytes 0-79) key 'missing' not found";
#if !JSON_DIAGNOSTICS
expected_missing += "(bytes 0-79) ";
#endif
expected_missing += "key 'missing' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing); CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
} }
+53
View File
@@ -12,7 +12,10 @@
using nlohmann::json; using nlohmann::json;
#include <algorithm> #include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string> #include <string>
#include <utility>
TEST_CASE("tests on very large JSONs") TEST_CASE("tests on very large JSONs")
{ {
@@ -140,6 +143,56 @@ TEST_CASE("tests on deeply nested JSONs")
} }
} }
SECTION("leaf values of every type below the bound of the recursive descent")
{
// the elements and members of the containers below the bound are
// copied one by one, whatever their type
const json leaves = json::array(
{
nullptr, true, false, -1, std::uint64_t{1}, 1.5, "text", json::binary({1, 2, 3})
});
const json members =
{
{"null", nullptr}, {"boolean", true}, {"integer", -1}, {"unsigned", std::uint64_t{1}},
{"float", 1.5}, {"string", "text"}, {"binary", json::binary({1, 2, 3})}
};
for (const bool objects :
{
false, true
})
{
CAPTURE(objects)
json j = objects ? members : leaves;
for (std::size_t i = 0; i < 300; ++i)
{
json wrapper = objects ? json::object() : json::array();
if (objects)
{
wrapper["a"] = std::move(j);
}
else
{
wrapper.push_back(std::move(j));
}
j = std::move(wrapper);
}
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
const json* current = &copy;
for (std::size_t i = 0; i < 300; ++i)
{
current = objects ? &current->at("a") : &current->front();
}
// the innermost value is shallow: it is safe to compare
CHECK(*current == (objects ? members : leaves));
CHECK(current->size() == (objects ? members.size() : leaves.size()));
}
}
SECTION("a value that is deep in one place only") SECTION("a value that is deep in one place only")
{ {
json j = json::object(); json j = json::object();
+203
View File
@@ -495,3 +495,206 @@ TEST_CASE("locale changes during a single dump() (#5709 item 3)")
// fix #5597 made on the parser side // fix #5597 made on the parser side
CHECK(buf.data == "[\"" + padding + "\",1234.5]"); CHECK(buf.data == "[\"" + padding + "\",1234.5]");
} }
namespace
{
// a token whose first data() call switches LC_NUMERIC, so that the locale
// changes between convert_float_locale_aware() looking up the decimal point
// and strtod reading the token (the race of #5198, made deterministic)
struct locale_switching_token
{
using value_type = char;
locale_switching_token(std::string text_, const char* switch_to_)
: text(std::move(text_)), switch_to(switch_to_)
{}
std::size_t size() const
{
return text.size();
}
char& operator[](std::size_t i)
{
return text[i];
}
char* data()
{
if (!switched)
{
switched = std::setlocale(LC_NUMERIC, switch_to) != nullptr;
}
return &text[0];
}
std::string text;
const char* switch_to;
bool switched = false;
};
// set LC_NUMERIC to the first of @a names that is usable and has a decimal
// point of @a min_size bytes at least (and at most @a max_size)
bool use_decimal_point_locale(const std::vector<const char*>& names, std::size_t min_size, std::size_t max_size, const char*& used)
{
for (const char* name : names)
{
if (std::setlocale(LC_NUMERIC, name) == nullptr)
{
continue;
}
const std::size_t size = std::string(std::localeconv()->decimal_point).size();
if (size >= min_size && size <= max_size)
{
used = name;
return true;
}
}
return false;
}
// convert a token with convert_float_locale_aware(); the token must be given
// back as it was
template<typename FloatType>
FloatType convert_locale_aware(const std::string& text)
{
std::string token = text;
const std::size_t dot = token.find('.');
FloatType value{};
nlohmann::detail::convert_float_locale_aware(token, dot, value);
CHECK(token == text);
return value;
}
template<typename FloatType>
void check_locale_aware_conversions()
{
CHECK(convert_locale_aware<FloatType>("3.5") == FloatType(3.5));
CHECK(convert_locale_aware<FloatType>("-0.25") == FloatType(-0.25));
CHECK(convert_locale_aware<FloatType>("1.25e2") == FloatType(125));
// no decimal point at all
CHECK(convert_locale_aware<FloatType>("12e1") == FloatType(120));
CHECK(convert_locale_aware<FloatType>("7") == FloatType(7));
}
} // namespace
TEST_CASE("convert_float_locale_aware")
{
const std::vector<const char*> single_byte_names = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", "ru_RU.UTF-8"};
const std::vector<const char*> multi_byte_names = {"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"};
const char* used = nullptr;
SECTION("decimal point '.'")
{
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
check_locale_aware_conversions<float>();
check_locale_aware_conversions<double>();
check_locale_aware_conversions<long double>();
}
SECTION("single-byte decimal point other than '.'")
{
if (use_decimal_point_locale(single_byte_names, 1, 1, used) && std::localeconv()->decimal_point[0] != '.')
{
CAPTURE(used)
check_locale_aware_conversions<float>();
check_locale_aware_conversions<double>();
check_locale_aware_conversions<long double>();
}
else
{
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
}
}
SECTION("multi-byte decimal point")
{
if (use_decimal_point_locale(multi_byte_names, 2, 8, used))
{
CAPTURE(used)
check_locale_aware_conversions<float>();
check_locale_aware_conversions<double>();
check_locale_aware_conversions<long double>();
}
else
{
MESSAGE("no locale with a multi-byte decimal point is usable");
}
}
SECTION("a token that is not converted completely")
{
// the locale does not change, so there is nothing to retry: the
// value up to the garbage is kept
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
std::string token = "1.5x";
double value = 0;
nlohmann::detail::convert_float_locale_aware(token, token.find('.'), value);
CHECK(value == 1.5);
CHECK(token == "1.5x");
}
SECTION("the locale changes before the conversion")
{
if (!use_decimal_point_locale(single_byte_names, 1, 1, used) || std::localeconv()->decimal_point[0] == '.')
{
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
}
else
{
const std::string comma_locale = used;
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
// "C" -> comma locale: strtod stops at the '.', so the conversion is repeated
{
locale_switching_token token("1.5", comma_locale.c_str());
double value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 1.5);
CHECK(token.text == "1.5");
}
}
// the same for the other floating-point types
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
{
locale_switching_token token("2.25", comma_locale.c_str());
float value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 2.25f);
}
}
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
{
locale_switching_token token("2.25", comma_locale.c_str());
long double value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 2.25L);
}
}
// comma locale -> multi-byte decimal point
const char* multi_byte = nullptr;
if (use_decimal_point_locale(multi_byte_names, 2, 8, multi_byte))
{
REQUIRE(std::setlocale(LC_NUMERIC, comma_locale.c_str()) != nullptr);
locale_switching_token token("1.5", multi_byte);
double value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 1.5);
CHECK(token.text == "1.5");
}
}
}
}
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
}
+30
View File
@@ -18,6 +18,7 @@ using nlohmann::json;
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
#include <cstddef> #include <cstddef>
#include <string>
#include <vector> #include <vector>
// Test suite for verifying MessagePack handling with std::byte input // Test suite for verifying MessagePack handling with std::byte input
@@ -96,4 +97,33 @@ TEST_CASE("MessagePack with std::byte")
} }
} }
} }
// JSON text (not MessagePack) as std::byte input: the error position is
// computed from the bytes consumed so far
TEST_CASE("JSON text with std::byte")
{
const auto to_bytes = [](const std::string & text)
{
std::vector<std::byte> bytes;
for (const char c : text)
{
bytes.push_back(static_cast<std::byte>(c));
}
return bytes;
};
SECTION("valid text")
{
const json j = json::parse(to_bytes("[1,\n2]"));
CHECK(j == json({1, 2}));
CHECK(json::accept(to_bytes("[1,\n2]")));
}
SECTION("invalid text")
{
CHECK_FALSE(json::accept(to_bytes("[1,\n2,")));
CHECK_THROWS_WITH_AS(json::parse(to_bytes("[1,\n2,")),
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", json::parse_error&);
}
}
#endif #endif
+101
View File
@@ -1648,6 +1648,30 @@ TEST_CASE("MessagePack")
} }
} }
} }
SECTION("SAX aborts")
{
SECTION("start_array(len)")
{
std::vector<uint8_t> const v = {0x93, 0x01, 0x02, 0x03};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("start_object(len)")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("key()")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(1);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
}
} }
TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays") TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays")
@@ -1718,6 +1742,21 @@ TEST_CASE("issue #5405 - array reserve for definite-length MessagePack arrays")
CHECK(json::from_msgpack(packed) == j); CHECK(json::from_msgpack(packed) == j);
} }
} }
SECTION("a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_msgpack(j);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::msgpack));
}
} }
TEST_CASE("regression test - MessagePack ext type rejects a subtype that doesn't fit a single byte") TEST_CASE("regression test - MessagePack ext type rejects a subtype that doesn't fit a single byte")
@@ -1756,6 +1795,15 @@ TEST_CASE("MessagePack nesting does not consume the call stack")
CHECK(json::from_msgpack(input, true, false).is_discarded()); CHECK(json::from_msgpack(input, true, false).is_discarded());
} }
SECTION("a well-formed deep value is read through the SAX interface")
{
std::vector<uint8_t> input(300000, 0x91);
input.push_back(0x01); // innermost value
SaxCountdown accept_all(600001);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::msgpack));
}
SECTION("a well-formed deep value is read into a value") SECTION("a well-formed deep value is read into a value")
{ {
const std::size_t depth = 10000; const std::size_t depth = 10000;
@@ -1803,6 +1851,31 @@ TEST_CASE("MessagePack input that cannot be read is discarded by every overload"
#endif #endif
} }
TEST_CASE("MessagePack SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::msgpack))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_msgpack(j)) == 20);
}
TEST_CASE("single MessagePack roundtrip") TEST_CASE("single MessagePack roundtrip")
{ {
SECTION("sample.json") SECTION("sample.json")
@@ -2238,3 +2311,31 @@ TEST_CASE("MessagePack arrays of scalars from contiguous and non-contiguous inpu
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack)); == json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
} }
} }
TEST_CASE("MessagePack arrays of one element from contiguous and non-contiguous input")
{
// contiguous input leaves an array of one element to the general path
// rather than to the loop that reads arrays of scalars; both inputs must
// still give the same events, value, or error for the complete document
// and for every truncation of it, with enough bytes after each array that
// the loop could have been entered
const json j = json::parse(R"({"a": [[[[1.5]]]], "b": [7], "c": [{"x": 2.5}], "d": ["s"], "e": [[1, 2]], "f": [null], "g": 1.25})");
const std::vector<std::uint8_t> bytes = json::to_msgpack(j);
CHECK(json::from_msgpack(bytes) == j);
for (std::size_t length = 0; length <= bytes.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack, true, false)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack, true, false));
CHECK(from_contiguous.events == from_bytewise.events);
}
}
+86
View File
@@ -16,6 +16,7 @@ using nlohmann::ordered_json;
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <vector>
TEST_CASE("ordered_json") TEST_CASE("ordered_json")
{ {
@@ -515,3 +516,88 @@ TEST_CASE("parsing random trees of arrays and objects")
CHECK(json::parse(text).dump() == expected.dump()); CHECK(json::parse(text).dump() == expected.dump());
} }
} }
TEST_CASE("object_key_index")
{
// the hash table of the DOM parser finds the position of a key in an
// ordered_json object; the parser creates it for exactly 64 elements, but
// it is correct for any number of elements it is given
using nlohmann::detail::object_key_index;
const std::size_t not_found = object_key_index::npos;
for (const std::size_t initial :
{
std::size_t{0}, std::size_t{10}, std::size_t{64}, std::size_t{65}, std::size_t{300}
})
{
CAPTURE(initial)
// the hashes are not distinct on purpose: elements with the same
// hash are told apart by the caller
const auto hash_of = [](std::size_t position) -> std::size_t
{
// truncated to 32 bits where std::size_t is that small
return static_cast<std::size_t>((position % 50) * 0x9E3779B97F4A7C15ull);
};
std::vector<std::size_t> hashes;
for (std::size_t i = 0; i < initial; ++i)
{
hashes.push_back(hash_of(i));
}
object_key_index index(std::move(hashes));
// the elements that are there
for (std::size_t i = 0; i < initial; ++i)
{
CHECK(index.find(hash_of(i), [i](std::size_t position)
{
return position == i;
}) == i);
}
CHECK(index.find(hash_of(1), [](std::size_t /*position*/)
{
return false;
}) == not_found);
// elements that are added later, until the table grows
for (std::size_t i = initial; i < initial + 600; ++i)
{
index.add(hash_of(i));
CHECK(index.find(hash_of(i), [i](std::size_t position)
{
return position == i;
}) == i);
}
CHECK(index.find(hash_of(initial), [initial](std::size_t position)
{
return position == initial;
}) == initial);
}
}
TEST_CASE("JSON pointers on ordered_json")
{
using pointer = ordered_json::json_pointer;
SECTION("operator[] inserts a missing key at the end")
{
ordered_json j = {{"z", 1}, {"x", {{"y", 2}}}};
j[pointer("/a")] = 3;
j[pointer("/x/b")] = 4;
CHECK(j == ordered_json({{"z", 1}, {"x", {{"y", 2}, {"b", 4}}}, {"a", 3}}));
CHECK(j.dump() == R"({"z":1,"x":{"y":2,"b":4},"a":3})");
}
SECTION("unflatten of a flattened object whose keys are not sorted")
{
// the prefixes /a and /c denote arrays; a key of /a follows one of /c
const ordered_json flat = {{"/a/0/p", 1}, {"/c/0/x", 2}, {"/a/0/q", 3}, {"/a/1/p", 4}};
const ordered_json expected = {{"a", {{{"p", 1}, {"q", 3}}, {{"p", 4}}}}, {"c", {{{"x", 2}}}}};
CHECK(flat.unflatten() == expected);
CHECK(expected.flatten().unflatten() == expected);
}
}
+72
View File
@@ -23,11 +23,13 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif #endif
#include <array>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <list> #include <list>
#include <limits> #include <limits>
#include <cstdio> #include <cstdio>
#include <utility>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
@@ -1519,6 +1521,76 @@ TEST_CASE("regression tests 1")
} }
} }
SECTION("operator[](json_pointer) continues with the member that emplace() found")
{
// see the section above: the lookup does not find "k", but emplace()
// finds "x" and the pointer continues with the member of "x". Here
// the member is a container, so the pointer can go on below it. Only
// the outermost object may be a copy (with restarted timestamps): the
// value of "x" is moved in, so that its own lookups work.
const my_json orig = my_json::parse(R"({"x":null})");
const auto make = [&orig]()
{
my_json::object_t top = orig.get_ref<const my_json::object_t&>();
top.at("x") = my_json::parse(R"({"a":{"b":1},"n":null,"arr":[10,20],"objs":[{"m":5},{"m":6}]})");
return my_json(std::move(top));
};
SECTION("existing members")
{
// emplace() registers the key "k" in the object, so that every
// pointer needs a new object to reach the member of "x" again
const std::array<std::pair<const char*, const char*>, 4> pointers = {{{"/k/a/b", "1"}, {"/k/arr/1", "20"}, {"/k/n", "null"}, {"/k/objs/1/m", "6"}}};
for (const auto& pointer : pointers)
{
CAPTURE(pointer.first)
my_json j = make();
CHECK(j[my_json::json_pointer(pointer.first)].dump() == pointer.second);
CHECK(j.size() == 1);
CHECK(j["x"].size() == 4);
}
}
SECTION("a missing member of an object")
{
my_json j = make();
#ifdef JSON_NO_THREAD_LOCAL
// (set by the user, or by the library for clang with MinGW)
// the objects below the outermost one are not copied by their
// object_t here, but member by member, which keeps their
// timestamps correct: "c" is not mistaken for "b" and is inserted
CHECK(j[my_json::json_pointer("/k/a/c/d")].is_null());
CHECK(j["x"]["a"].dump() == R"({"b":1,"c":{"d":null}})");
#else
// the timestamp of "c" is that of "b" again, so that emplace()
// finds the member "b", a number, which has no member "d"
CHECK_THROWS_AS(j[my_json::json_pointer("/k/a/c/d")], my_json::out_of_range&);
CHECK(j["x"]["a"].dump() == R"({"b":1})");
#endif
}
SECTION("a null value is converted to an object")
{
my_json j = make();
j[my_json::json_pointer("/k/n/c")] = 3;
CHECK(j["x"]["n"].dump() == R"({"c":3})");
}
SECTION("the end of an array")
{
my_json j = make();
j[my_json::json_pointer("/k/arr/-")] = 30;
CHECK(j["x"]["arr"].dump() == "[10,20,30]");
}
SECTION("an index past the end of an array")
{
my_json j = make();
j[my_json::json_pointer("/k/arr/3")] = 40;
CHECK(j["x"]["arr"].dump() == "[10,20,null,40]");
}
}
SECTION("issue #977 - Assigning between different json types") SECTION("issue #977 - Assigning between different json types")
{ {
foo_json lj = ns::foo{3}; foo_json lj = ns::foo{3};
+4
View File
@@ -441,6 +441,10 @@ TEST_CASE("regression tests 2")
float_json j2 = {1000.0, 2000.0, 3000.0}; float_json j2 = {1000.0, 2000.0, 3000.0};
CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2); CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2);
// half-precision floats of both signs are converted to the float type
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0xC0, 0x00}) == -2.0f);
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0x3C, 0x00}) == 1.0f);
} }
SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?") SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?")
+52
View File
@@ -22,6 +22,14 @@
// scoped enum, so get<std::byte>() (needed below to get<std::vector<std::byte>>() // scoped enum, so get<std::byte>() (needed below to get<std::vector<std::byte>>()
// from a plain JSON array, not just from an already-binary value) relies on // from a plain JSON array, not just from an already-binary value) relies on
// enum serialization being enabled // enum serialization being enabled
// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command
// line *before* including json.hpp, since the library #undefs it once the header
// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the
// tests of deprecated functions are skipped if these functions are deleted
#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1)
#define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
#endif
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1) #if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION #define SKIP_TESTS_FOR_ENUM_SERIALIZATION
#endif #endif
@@ -802,6 +810,50 @@ TEST_CASE("issue #5338 - truncated CBOR tagged binary subtype is rejected")
} }
} }
TEST_CASE("issue #5676 - SAX parsing of CBOR tags")
{
const json expected = json::binary({1, 2, 3}, 42);
const auto cbor = json::to_cbor(expected);
nlohmann::detail::json_sax_acceptor<json> acceptor;
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor));
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::error));
CHECK(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::ignore));
json parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(parsed);
CHECK(json::sax_parse(cbor, &sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(parsed == expected);
json iterator_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> iterator_sax(iterator_parsed);
CHECK(json::sax_parse(cbor.begin(), cbor.end(), &iterator_sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(iterator_parsed == expected);
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
json span_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> span_sax(span_parsed);
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(cbor.data(), cbor.size()), &span_sax,
json::input_format_t::cbor, true, false, false, json::cbor_tag_handler_t::store));
CHECK(span_parsed == expected);
#endif
const std::string text = "null";
CHECK(json::sax_parse(text, &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(json::sax_parse(text.begin(), text.end(), &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(text.data(), text.size()), &acceptor,
json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store));
#endif
}
TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with an object") TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with an object")
{ {
json t = {{"k", 1}}; json t = {{"k", 1}};
-781
View File
@@ -1,781 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
/////////////////////////////////////////////////////////////////////
// Tests that call basic_json::sax_parse have a file of their own: every
// sax_parse call instantiates the parser and binary reader that recover from
// errors (see #3989), and in unit-regression2.cpp, unit-regression3.cpp, and
// unit-msgpack.cpp this made the objects too large for the MinGW linker to
// relocate (see #5511).
/////////////////////////////////////////////////////////////////////
#include "doctest_compatibility.h"
// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command
// line *before* including json.hpp, since the library #undefs it once the header
// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the
// tests of deprecated functions are skipped if these functions are deleted
#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1)
#define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
#endif
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "sax_countdown.hpp"
using utils::SaxCountdown;
// a narrow number_float_t, so that a double read from binary input can
// overflow it
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
namespace
{
/// builds a value from SAX events, asks the parser to recover from its first
/// 100 errors, and checks that the events are balanced (see #3989)
template<typename BasicJsonType>
class BasicRecoveringParser
{
public:
explicit BasicRecoveringParser(BasicJsonType& j)
: dom(j, false)
{}
bool null()
{
value();
return dom.null();
}
bool boolean(bool val)
{
value();
return dom.boolean(val);
}
bool number_integer(typename BasicJsonType::number_integer_t val)
{
value();
return dom.number_integer(val);
}
bool number_unsigned(typename BasicJsonType::number_unsigned_t val)
{
value();
return dom.number_unsigned(val);
}
bool number_float(typename BasicJsonType::number_float_t val, const std::string& s)
{
value();
return dom.number_float(val, s);
}
bool string(std::string& val)
{
value();
return dom.string(val);
}
bool binary(typename BasicJsonType::binary_t& val)
{
value();
return dom.binary(val);
}
bool start_object(std::size_t elements)
{
value();
stack.push_back('o');
return dom.start_object(elements);
}
bool key(std::string& val)
{
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.back() = 'v';
return dom.key(val);
}
bool end_object()
{
if (stack.empty() || stack.back() != 'o')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_object();
}
bool start_array(std::size_t elements)
{
value();
stack.push_back('a');
return dom.start_array(elements);
}
bool end_array()
{
if (stack.empty() || stack.back() != 'a')
{
well_formed = false;
return false;
}
stack.pop_back();
return dom.end_array();
}
bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& ex)
{
messages.emplace_back(ex.what());
// a limit, so that a reader that does not stop fails the test
// instead of making it hang
return ++errors < 100;
}
/// whether the events were balanced and every key was followed by a value
bool balanced() const
{
return well_formed && stack.empty();
}
/// builds the value
nlohmann::detail::json_sax_dom_parser<BasicJsonType> dom;
std::size_t errors = 0;
std::vector<std::string> messages {}; // NOLINT(readability-redundant-member-init)
std::vector<char> stack {}; // NOLINT(readability-redundant-member-init)
bool well_formed = true;
private:
void value()
{
if (!stack.empty())
{
if (stack.back() == 'v')
{
stack.back() = 'o';
}
else if (stack.back() == 'o')
{
well_formed = false;
}
}
}
};
using RecoveringParser = BasicRecoveringParser<json>;
struct BinaryParseResult
{
json value;
std::size_t errors;
std::vector<std::string> messages;
bool ok;
bool balanced;
};
BinaryParseResult parse_binary_recovering(const std::vector<std::uint8_t>& input, const json::input_format_t format)
{
json j;
RecoveringParser sax(j);
const bool ok = json::sax_parse(input, &sax, format);
return {j, sax.errors, sax.messages, ok, sax.balanced()};
}
#if !defined(JSON_NOEXCEPTION)
/// the message of the exception that reading @a input into a JSON value
/// throws, or an empty string if reading succeeds
std::string binary_error_message(const std::vector<std::uint8_t>& input, const json::input_format_t format)
{
try
{
json _;
switch (format)
{
case json::input_format_t::cbor:
_ = json::from_cbor(input);
break;
case json::input_format_t::msgpack:
_ = json::from_msgpack(input);
break;
case json::input_format_t::ubjson:
_ = json::from_ubjson(input);
break;
case json::input_format_t::bjdata:
_ = json::from_bjdata(input);
break;
case json::input_format_t::bson:
_ = json::from_bson(input);
break;
case json::input_format_t::bon8:
_ = json::from_bon8(input);
break;
case json::input_format_t::json:
default:
break;
}
}
catch (const json::exception& e)
{
return e.what();
}
return "";
}
#endif
/// a BSON element: its type, its name, and its value
std::vector<std::uint8_t> bson_element(const std::uint8_t type, const std::string& name, const std::vector<std::uint8_t>& value)
{
std::vector<std::uint8_t> result = {type};
result.insert(result.end(), name.begin(), name.end());
result.push_back(0x00);
result.insert(result.end(), value.begin(), value.end());
return result;
}
/// a BSON document of the given elements; @a size_offset is added to the
/// size it declares
std::vector<std::uint8_t> bson_document(const std::vector<std::vector<std::uint8_t>>& elements, const int size_offset = 0)
{
std::vector<std::uint8_t> body;
for (const auto& element : elements)
{
body.insert(body.end(), element.begin(), element.end());
}
const auto size = static_cast<std::uint32_t>(static_cast<int>(body.size()) + 5 + size_offset);
std::vector<std::uint8_t> result = {static_cast<std::uint8_t>(size & 0xFFu), static_cast<std::uint8_t>((size >> 8u) & 0xFFu),
static_cast<std::uint8_t>((size >> 16u) & 0xFFu), static_cast<std::uint8_t>((size >> 24u) & 0xFFu)
};
result.insert(result.end(), body.begin(), body.end());
result.push_back(0x00);
return result;
}
/// a BSON int32 value
std::vector<std::uint8_t> bson_int32(const std::int32_t value)
{
const auto u = static_cast<std::uint32_t>(value);
return {static_cast<std::uint8_t>(u & 0xFFu), static_cast<std::uint8_t>((u >> 8u) & 0xFFu),
static_cast<std::uint8_t>((u >> 16u) & 0xFFu), static_cast<std::uint8_t>((u >> 24u) & 0xFFu)};
}
/// a BSON string value, whose length is @a length_offset off
std::vector<std::uint8_t> bson_string(const std::string& value, const std::int32_t length_offset = 0)
{
auto result = bson_int32(static_cast<std::int32_t>(value.size() + 1) + length_offset);
result.insert(result.end(), value.begin(), value.end());
result.push_back(0x00);
return result;
}
/// @a count bytes of value 0xAB
std::vector<std::uint8_t> bytes(const std::size_t count)
{
return std::vector<std::uint8_t>(count, 0xAB);
}
/// the bytes of @a parts, one after the other
std::vector<std::uint8_t> concatenated(std::initializer_list<std::vector<std::uint8_t>> parts)
{
std::vector<std::uint8_t> result;
for (const auto& part : parts)
{
result.insert(result.end(), part.begin(), part.end());
}
return result;
}
/// U+FFFD REPLACEMENT CHARACTER
std::string replacement_character()
{
return "\xEF\xBF\xBD";
}
} // namespace
TEST_CASE("regression test - #3989 SAX parse_error() returning true")
{
SECTION("binary formats complete what was read before the input ends")
{
const json j = {{"a", {1, -2, {{"b", "c"}}, json::array()}}, {"d", {{"e", nullptr}, {"f", true}}}, {"g", 1.5}, {"h", json::binary({1, 2, 3})}};
const std::vector<std::pair<json::input_format_t, std::vector<std::uint8_t>>> encodings =
{
{json::input_format_t::cbor, json::to_cbor(j)},
{json::input_format_t::msgpack, json::to_msgpack(j)},
{json::input_format_t::ubjson, json::to_ubjson(j)},
{json::input_format_t::ubjson, json::to_ubjson(j, true, true)},
{json::input_format_t::bjdata, json::to_bjdata(j)},
{json::input_format_t::bjdata, json::to_bjdata(j, true, true)},
{json::input_format_t::bson, json::to_bson(j)},
{json::input_format_t::bon8, json::to_bon8(j)},
};
for (const auto& encoding : encodings)
{
const auto format = encoding.first;
const auto& bytes = encoding.second;
CAPTURE(format)
// every prefix is truncated input
for (std::size_t length = 0; length < bytes.size(); ++length)
{
CAPTURE(length)
const auto result = parse_binary_recovering(std::vector<std::uint8_t>(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(length)), format);
CHECK(!result.ok);
CHECK(result.errors == 1);
CHECK(result.balanced);
}
// the complete input is read as usual (binary values do not
// round-trip through every format, so compare with a plain parse)
json expected;
nlohmann::detail::json_sax_dom_parser<json> dom(expected);
CHECK(json::sax_parse(bytes, &dom, format));
const auto complete = parse_binary_recovering(bytes, format);
CHECK(complete.ok);
CHECK(complete.errors == 0);
CHECK(complete.value == expected);
// a byte after the value
auto trailing_bytes = bytes;
trailing_bytes.push_back(0x01);
const auto trailing = parse_binary_recovering(trailing_bytes, format);
CHECK(!trailing.ok);
CHECK(trailing.errors == 1);
CHECK(trailing.value == expected);
}
}
SECTION("containers without an end")
{
// these made the readers loop, or read on, after the error
const auto cbor_array = parse_binary_recovering({0x9F}, json::input_format_t::cbor);
CHECK(cbor_array.errors == 1);
CHECK(cbor_array.value == json::array());
const auto cbor_map = parse_binary_recovering({0xBF, 0x61, 'a'}, json::input_format_t::cbor);
CHECK(cbor_map.errors == 1);
CHECK(cbor_map.value == json({{"a", nullptr}}));
const auto msgpack_array = parse_binary_recovering({0xDD, 0xFF, 0xFF, 0xFF, 0xFF}, json::input_format_t::msgpack);
CHECK(msgpack_array.errors == 1);
CHECK(msgpack_array.value == json::array());
const auto msgpack_map = parse_binary_recovering({0x81, 0xA1, 'a', 0x92, 0x01}, json::input_format_t::msgpack);
CHECK(msgpack_map.errors == 1);
CHECK(msgpack_map.value == json({{"a", {1}}}));
}
SECTION("BJData ndarray")
{
// a 2x3 int8 array with two of its six elements; the annotated array
// format opens an object and two arrays of its own
const auto result = parse_binary_recovering({'[', '$', 'i', '#', '[', '$', 'i', '#', 'i', 2, 2, 3, 1, 2}, json::input_format_t::bjdata);
CHECK(result.errors == 1);
CHECK(result.balanced);
CHECK(result.value == json({{"_ArrayType_", "int8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2}}}));
}
SECTION("binary formats repair items whose end is known")
{
struct Repair
{
json::input_format_t format;
std::vector<std::uint8_t> input;
json expected;
std::size_t errors;
};
const std::vector<Repair> repairs =
{
// CBOR: tags are ignored (here tag 1 and the self-describe tag 55799)
{json::input_format_t::cbor, {0x82, 0xC1, 0x05, 0xD9, 0xD9, 0xF7, 0x06}, {5, 6}, 2},
// CBOR: undefined and other simple values become null
{json::input_format_t::cbor, {0x84, 0xF7, 0xE0, 0xF8, 0x20, 0x01}, {nullptr, nullptr, nullptr, 1}, 3},
// CBOR: members whose key is not a string are skipped, whatever their key and value
{json::input_format_t::cbor, {0xA4, 0x01, 0x02, 0x82, 0x01, 0x02, 0xA1, 0x61, 'x', 0x9F, 0xFF, 0xC1, 0x01, 0x5F, 0x41, 0x00, 0xFF, 0x61, 'a', 0x03}, {{"a", 3}}, 3},
{json::input_format_t::cbor, {0xBF, 0xF5, 0xBF, 0x61, 'x', 0x7F, 0x61, 'y', 0xFF, 0xFF, 0x61, 'a', 0x03, 0xFF}, {{"a", 3}}, 1},
// MessagePack: members whose key is not a string are skipped
{json::input_format_t::msgpack, {0x84, 0x01, 0x02, 0x81, 0xA1, 'x', 0x01, 0x92, 0x01, 0x02, 0xD4, 0x01, 0x02, 0xC0, 0xA1, 'a', 0x04}, {{"a", 4}}, 3},
// UBJSON: a char that is not ASCII becomes U+FFFD
{json::input_format_t::ubjson, {'[', 'C', 0x80, 'C', 'A', ']'}, {replacement_character(), "A"}, 1},
// UBJSON: the longest beginning of a high-precision number is kept
{json::input_format_t::ubjson, {'[', 'H', 'i', 5, '1', '2', 'a', 'b', 'c', 'H', 'i', 2, '1', '.', 'H', 'i', 3, 'a', 'b', 'c', 'H', 'i', 3, '4', '.', '5', ']'}, {12, 1, nullptr, 4.5}, 3},
// BJData, too
{json::input_format_t::bjdata, {'[', 'C', 0xFF, 'H', 'i', 2, '-', '1', 'H', 'i', 2, '-', 'x', ']'}, {replacement_character(), -1, nullptr}, 2},
// a NUL ends a high-precision number, as it ends JSON text
{json::input_format_t::ubjson, {'[', 'H', 'i', 4, '1', '2', 0, '9', 'H', 'i', 2, 0, '1', 'i', 3, ']'}, {12, nullptr, 3}, 2},
// BON8: members whose key is not a string are skipped
{json::input_format_t::bon8, {0x89, 0x91, 0x92, 0xC9, 0x40, 0x82, 0x91, 0x92, 0x61, 0x93}, {{"a", 3}}, 2},
{json::input_format_t::bon8, {0x8B, 0x91, 0x85, 0x91, 0xFE, 0xFA, 0x8B, 'x', 0x91, 0xFE, 0x61, 0x93, 0xFE}, {{"a", 3}}, 2},
// BSON: elements of types the library does not read become null
{
json::input_format_t::bson, bson_document(
{
bson_element(0x07, "_id", bytes(12)), // ObjectId
bson_element(0x09, "date", bytes(8)), // UTC datetime
bson_element(0x13, "decimal", bytes(16)), // 128-bit decimal
bson_element(0x0B, "regex", {'a', '+', 0, 'i', 0}), // regular expression
bson_element(0x0D, "code", bson_string("f()")), // JavaScript code
bson_element(0x0E, "symbol", bson_string("s")), // symbol
bson_element(0x0C, "pointer", concatenated({bson_string("c"), bytes(12)})), // DBPointer
bson_element(0x0F, "scope", concatenated({bson_int32(15), bson_string("g"), bson_document({})})), // code with scope
bson_element(0x06, "undefined", {}), // undefined
bson_element(0xFF, "min", {}), // min key
bson_element(0x7F, "max", {}), // max key
bson_element(0x10, "z", bson_int32(7)),
}),
{{"_id", nullptr}, {"date", nullptr}, {"decimal", nullptr}, {"regex", nullptr}, {"code", nullptr}, {"symbol", nullptr}, {"pointer", nullptr}, {"scope", nullptr}, {"undefined", nullptr}, {"min", nullptr}, {"max", nullptr}, {"z", 7}},
11
},
// BSON: an element of an unknown type becomes null, and the rest of its document is skipped
{
json::input_format_t::bson, bson_document(
{
bson_element(0x03, "inner", bson_document({bson_element(0x10, "a", bson_int32(1)), bson_element(0x42, "x", bytes(3)), bson_element(0x10, "b", bson_int32(2))})),
bson_element(0x04, "array", bson_document({bson_element(0x10, "0", bson_int32(1)), bson_element(0x42, "1", bytes(3))})),
bson_element(0x10, "after", bson_int32(3)),
}),
{{"inner", {{"a", 1}, {"x", nullptr}}}, {"array", {1, nullptr}}, {"after", 3}},
2
},
// BSON: so does a string or byte array whose length cannot be right
{
json::input_format_t::bson, bson_document(
{
bson_element(0x03, "inner", bson_document({bson_element(0x02, "s", bson_string("abc", -10)), bson_element(0x10, "b", bson_int32(2))})),
bson_element(0x03, "bin", bson_document({bson_element(0x05, "b", concatenated({bson_int32(-1), bytes(1)})), bson_element(0x10, "b", bson_int32(2))})),
bson_element(0x10, "after", bson_int32(3)),
}),
{{"inner", {{"s", nullptr}}}, {"bin", {{"b", nullptr}}}, {"after", 3}},
2
},
// BSON: a string without its terminator, and a document whose size does not match, are kept
{
json::input_format_t::bson, bson_document(
{
bson_element(0x02, "s", {2, 0, 0, 0, 'a', 'X'}),
bson_element(0x03, "inner", bson_document({bson_element(0x10, "a", bson_int32(1))}, 1)),
}),
{{"s", "a"}, {"inner", {{"a", 1}}}},
2
},
// BSON: so is a document whose size is too small for its content,
// which is read up to its terminator
{
json::input_format_t::bson, bson_document(
{
bson_element(0x03, "inner", bson_document({bson_element(0x10, "a", bson_int32(1)), bson_element(0x10, "b", bson_int32(2))}, -7)),
bson_element(0x10, "after", bson_int32(3)),
}),
{{"inner", {{"a", 1}, {"b", 2}}}, {"after", 3}},
1
},
};
for (const auto& repair : repairs)
{
CAPTURE(repair.format)
CAPTURE(repair.input)
const auto result = parse_binary_recovering(repair.input, repair.format);
CHECK(!result.ok);
CHECK(result.balanced);
CHECK(result.errors == repair.errors);
CHECK(result.value == repair.expected);
REQUIRE(!result.messages.empty());
#if !defined(JSON_NOEXCEPTION)
// the first error is the one reported without recovering; under
// JSON_NOEXCEPTION, reading without recovering aborts instead of
// throwing, so there is no message to compare with
CHECK(result.messages.front() == binary_error_message(repair.input, repair.format));
#endif
}
}
SECTION("binary formats repair numbers that are out of range")
{
// CBOR: a double too large for a float number_float_t
float_json cbor;
BasicRecoveringParser<float_json> sax(cbor);
const std::vector<std::uint8_t> cbor_input = {0x82, 0xFB, 0x7E, 0x37, 0xE4, 0x3C, 0x88, 0x00, 0x75, 0x9C, 0x01}; // [1e300, 1]
CHECK(!float_json::sax_parse(cbor_input, &sax, float_json::input_format_t::cbor));
CHECK(sax.errors == 1);
CHECK(sax.messages.front() == "[json.exception.out_of_range.406] syntax error while parsing CBOR value: number overflow");
REQUIRE(cbor.size() == 2);
CHECK(std::isinf(cbor[0].get<float>()));
CHECK(cbor[1] == 1);
// UBJSON: a high-precision number too large for number_float_t
const auto ubjson = parse_binary_recovering({'H', 'i', 5, '1', 'e', '9', '9', '9'}, json::input_format_t::ubjson);
CHECK(ubjson.errors == 1);
CHECK(ubjson.value.is_number_float());
CHECK(std::isinf(ubjson.value.get<double>()));
}
SECTION("binary formats stop where the end of an item is not known")
{
// a byte that begins no item
const auto cbor = parse_binary_recovering({0x82, 0x01, 0x1C, 0x02}, json::input_format_t::cbor);
CHECK(cbor.errors == 1);
CHECK(cbor.value == json({1}));
// a key that is no item: the unused MessagePack byte, a CBOR break
// in a map of known size, and the end of a BON8 container
const auto msgpack = parse_binary_recovering({0x82, 0xA1, 'a', 0x01, 0xC1, 0x02}, json::input_format_t::msgpack);
CHECK(msgpack.errors == 1);
CHECK(msgpack.value == json({{"a", 1}}));
const auto cbor_break = parse_binary_recovering({0xA2, 0x61, 'a', 0x01, 0xFF, 0x02}, json::input_format_t::cbor);
CHECK(cbor_break.errors == 1);
CHECK(cbor_break.value == json({{"a", 1}}));
const auto bon8 = parse_binary_recovering({0x88, 0x61, 0x91, 0xFE}, json::input_format_t::bon8);
CHECK(bon8.errors == 1);
CHECK(bon8.value == json({{"a", 1}}));
// an indefinite-length string inside an indefinite-length string
const auto nested = parse_binary_recovering({0x82, 0x01, 0x7F, 0x7F, 0x61, 'a', 0xFF, 0xFF}, json::input_format_t::cbor);
CHECK(nested.errors == 1);
CHECK(nested.value == json({1}));
// a BJData ndarray whose element type is not valid: the type is
// checked before the object that would hold the ndarray is begun
const auto ndarray = parse_binary_recovering({'[', '$', 0x01, '#', '[', '$', 'i', '#', 'i', 2, 2, 3}, json::input_format_t::bjdata);
CHECK(ndarray.errors == 1);
CHECK(ndarray.balanced);
CHECK(ndarray.value == nullptr);
// a skipped member that the input ends in
const auto truncated = parse_binary_recovering({0xA2, 0x01, 0x82, 0x01}, json::input_format_t::cbor);
CHECK(truncated.errors == 2);
CHECK(truncated.balanced);
CHECK(truncated.value == json::object());
// a BSON element of an unknown type in a document whose size cannot be
// right: the size is reported when it is read, and the element ends
// the document, as its end is not known
const auto bson = parse_binary_recovering(bson_document({bson_element(0x10, "a", bson_int32(1)), bson_element(0x42, "x", bytes(3))}, -10), json::input_format_t::bson);
CHECK(bson.errors == 2);
CHECK(bson.value == json({{"a", 1}, {"x", nullptr}}));
}
SECTION("changed bytes in binary input")
{
const json j = {{"a", {1, -2, {{"b", "c"}}, json::array()}}, {"d", {{"e", nullptr}, {"f", true}}}, {"g", 1.5}, {"h", json::binary({1, 2, 3})}, {"i", "\xC3\xA4"}};
const std::vector<std::pair<json::input_format_t, std::vector<std::uint8_t>>> encodings =
{
{json::input_format_t::cbor, json::to_cbor(j)},
{json::input_format_t::msgpack, json::to_msgpack(j)},
{json::input_format_t::ubjson, json::to_ubjson(j)},
{json::input_format_t::ubjson, json::to_ubjson(j, true, true)},
{json::input_format_t::bjdata, json::to_bjdata(j)},
{json::input_format_t::bjdata, json::to_bjdata(j, true, true)},
{json::input_format_t::bson, json::to_bson(j)},
{json::input_format_t::bon8, json::to_bon8(j)},
};
const std::vector<std::uint8_t> replacements = {0x00, 0x01, 0x7F, 0x80, 0xC1, 0xD9, 0xE0, 0xF7, 0xFE, 0xFF};
for (const auto& encoding : encodings)
{
const auto format = encoding.first;
const auto& original = encoding.second;
CAPTURE(format)
std::vector<std::vector<std::uint8_t>> inputs;
for (std::size_t position = 0; position < original.size(); ++position)
{
for (const auto replacement : replacements)
{
auto changed = original;
changed[position] = replacement;
inputs.push_back(changed);
}
auto removed = original;
removed.erase(removed.begin() + static_cast<std::ptrdiff_t>(position));
inputs.push_back(removed);
}
for (const auto& input : inputs)
{
CAPTURE(input)
const auto result = parse_binary_recovering(input, format);
CHECK(result.balanced);
CHECK(result.errors <= input.size() + 1);
#if !defined(JSON_NOEXCEPTION)
// an error is reported exactly if reading into a JSON value
// fails, and the first one is the same (under JSON_NOEXCEPTION,
// that reading aborts instead of throwing)
const auto message = binary_error_message(input, format);
CHECK(result.ok == message.empty());
if (!result.ok && result.errors < 100)
{
CHECK(result.messages.front() == message);
}
#endif
}
}
}
SECTION("JSON text")
{
// the parser stopped, but reported success
json j;
RecoveringParser sax(j);
CHECK(!json::sax_parse("[1,2,3,]", &sax));
CHECK(sax.errors == 1);
CHECK(j == json({1, 2, 3}));
}
SECTION("the SAX parsers of the library stop")
{
json _;
CHECK(json::from_cbor(std::vector<std::uint8_t> {0x9F}, true, false).is_discarded());
CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector<std::uint8_t> {0x9F}), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&);
CHECK(json::parse("[1,2,3,]", nullptr, false).is_discarded());
CHECK(!json::accept("[1,2,3,]"));
}
}
TEST_CASE("issue #5676 - SAX parsing of CBOR tags")
{
const json expected = json::binary({1, 2, 3}, 42);
const auto cbor = json::to_cbor(expected);
nlohmann::detail::json_sax_acceptor<json> acceptor;
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor));
CHECK_FALSE(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::error));
CHECK(json::sax_parse(cbor, &acceptor, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::ignore));
json parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(parsed);
CHECK(json::sax_parse(cbor, &sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(parsed == expected);
json iterator_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> iterator_sax(iterator_parsed);
CHECK(json::sax_parse(cbor.begin(), cbor.end(), &iterator_sax, json::input_format_t::cbor,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(iterator_parsed == expected);
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
json span_parsed;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> span_sax(span_parsed);
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(cbor.data(), cbor.size()), &span_sax,
json::input_format_t::cbor, true, false, false, json::cbor_tag_handler_t::store));
CHECK(span_parsed == expected);
#endif
const std::string text = "null";
CHECK(json::sax_parse(text, &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
CHECK(json::sax_parse(text.begin(), text.end(), &acceptor, json::input_format_t::json,
true, false, false, json::cbor_tag_handler_t::store));
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(text.data(), text.size()), &acceptor,
json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store));
#endif
}
TEST_CASE("MessagePack SAX aborts")
{
SECTION("start_array(len)")
{
std::vector<uint8_t> const v = {0x93, 0x01, 0x02, 0x03};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("start_object(len)")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(0);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
SECTION("key()")
{
std::vector<uint8_t> const v = {0x81, 0xa3, 0x66, 0x6F, 0x6F, 0xc2};
SaxCountdown scp(1);
CHECK(!json::sax_parse(v, &scp, json::input_format_t::msgpack));
}
}
TEST_CASE("issue #5405 - a user-defined SAX consumer is unaffected by the internal DOM reserve optimization")
{
// the reserve() call is local to json_sax_dom_parser / json_sax_dom_callback_parser;
// a custom SAX consumer that does not touch a DOM array sees identical events
json j = json::array();
for (int i = 0; i < 100; ++i)
{
j.push_back(i);
}
const auto packed = json::to_msgpack(j);
SaxCountdown scp(1000000); // large enough to never trigger an abort
CHECK(json::sax_parse(packed, &scp, json::input_format_t::msgpack));
}
TEST_CASE("MessagePack nesting does not consume the call stack - SAX interface")
{
// see the test case of the same name in unit-msgpack.cpp (#5104)
std::vector<uint8_t> input(300000, 0x91);
input.push_back(0x01); // innermost value
SaxCountdown accept_all(600001);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::msgpack));
}
TEST_CASE("MessagePack SAX parsing stops at every event")
{
// Containers are opened and closed by the loop that reads them; a SAX
// handler that rejects any event - including the end of a nested
// container - must stop the parse right there.
const auto count_events = [](const std::vector<std::uint8_t>& input)
{
int events = 0;
while (true)
{
SaxCountdown scp(events);
if (json::sax_parse(input, &scp, json::input_format_t::msgpack))
{
return events;
}
++events;
REQUIRE(events < 1000);
}
};
// 20 events: every container kind closes inside another one
const json j = json::parse(R"({"a": [1, {"b": []}], "c": {"d": [[2]]}})");
CHECK(count_events(json::to_msgpack(j)) == 20);
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP
+58
View File
@@ -19,11 +19,13 @@
using nlohmann::json; using nlohmann::json;
#include <algorithm> #include <algorithm>
#include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <fstream> #include <fstream>
#include <istream> #include <istream>
#include <limits>
#include <random> #include <random>
#include <sstream> #include <sstream>
#include <streambuf> #include <streambuf>
@@ -93,6 +95,33 @@ class chunked_streambuf : public std::streambuf
std::size_t m_unbuffered; std::size_t m_unbuffered;
}; };
#if TEST_STREAM_BULK_SCAN
// A streambuf that claims a get area of several gigabytes, of which only the
// first bytes exist. It is only used to move the read position of the adapter,
// never to read beyond them, so that skipping more than gbump() can bump at
// once can be tested without allocating that much.
class oversized_get_area_streambuf : public std::streambuf
{
public:
explicit oversized_get_area_streambuf(std::size_t claimed_size)
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
setg(m_data.data(), m_data.data(), m_data.data() + claimed_size);
}
// Give the get area back its real size, so that the adapter may read the
// character at the read position when it is destroyed (with
// JSON_PRECISE_STREAM_POSITION, it consumes the last character returned).
void restore_get_area()
{
setg(m_data.data(), m_data.data(), m_data.data() + m_data.size());
}
private:
std::array<char, 8> m_data{{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}};
};
#endif
// how a test reads from the stream // how a test reads from the stream
enum class read_mode enum class read_mode
{ {
@@ -433,6 +462,35 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk")
ia.copy_token(token, false); ia.copy_token(token, false);
CHECK(token.empty()); CHECK(token.empty());
} }
SECTION("skipping more characters than gbump() takes at once")
{
// gbump() takes an int, but a get area can be larger
const std::size_t claimed = sizeof(std::size_t) < 8 ? 0 : std::size_t{3} << 30u;
oversized_get_area_streambuf buf(claimed);
if (claimed == 0 || static_cast<std::size_t>(buf.in_avail()) != claimed)
{
// a 32-bit size_t, or a standard library whose streambuf stores
// the size of the get area in an int (MSVC)
MESSAGE("a get area larger than an int is not supported");
}
else
{
std::istream is(&buf);
nlohmann::detail::input_stream_adapter ia(is);
CHECK(ia.get_character() == 'a');
CHECK(ia.bulk_remaining() == claimed - 1);
const std::size_t skipped = static_cast<std::size_t>((std::numeric_limits<int>::max)()) + 10;
ia.bulk_skip(skipped);
CHECK(ia.bulk_remaining() == claimed - 1 - skipped);
ia.bulk_skip(ia.bulk_remaining());
CHECK(ia.bulk_remaining() == 0);
buf.restore_get_area();
}
}
#endif #endif
SECTION("SAX parsing from a stream") SECTION("SAX parsing from a stream")
+46
View File
@@ -928,3 +928,49 @@ TEST_CASE("short decimals")
} }
} }
} }
TEST_CASE("decimals of up to 17 digits")
{
// write_decimal() lays out digits * 10^exp as format_buffer() does; to_chars()
// calls it for the doubles whose digits write_shortest() does not convert at
// once, and the digits may be as many as 17
const auto written = [](std::uint64_t digits, int exp) -> std::string
{
std::array<char, 64> buf{}; // (up to 41 bytes are written)
char* const end = nlohmann::detail::dtoa_impl::write_decimal(buf.data(), digits, exp);
return {buf.data(), end};
};
SECTION("fixed notation")
{
CHECK(written(1, 0) == "1.0");
CHECK(written(1234, -2) == "12.34");
CHECK(written(123, 2) == "12300.0");
CHECK(written(50000000000u, -1) == "5000000000.0");
CHECK(written(100000000u, -8) == "1.0");
CHECK(written(12345678u, -12) == "1.2345678e-05");
CHECK(written(1234, 11) == "123400000000000.0");
// the point moves left of the first digit
CHECK(written(5, -3) == "0.005");
CHECK(written(12345678901234567u, -20) == "0.00012345678901234567");
}
SECTION("exponent notation")
{
CHECK(written(5, -5) == "5e-05");
CHECK(written(1234, 12) == "1.234e+15");
CHECK(written(5, -324) == "5e-324");
}
SECTION("17 digits")
{
CHECK(written(12345678901234567u, -16) == "1.2345678901234567");
CHECK(written(12345678901234567u, -19) == "0.0012345678901234567");
CHECK(written(12345678901234567u, 5) == "1.2345678901234567e+21");
CHECK(written(99999999999999999u, -17) == "0.99999999999999999");
CHECK(written(10000000000000001u, -16) == "1.0000000000000001");
// trailing zeros are not written
CHECK(written(10000000000000000u, -17) == "0.1");
CHECK(written(12000000000000000u, -15) == "12.0");
}
}
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more