Compare commits

...
Author SHA1 Message Date
Niels Lohmann 2e81630c47 Merge branch 'perf/lexer-lazy-float-text' into perf/lexer-simd-strings
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:23:24 +02:00
Niels Lohmann 48d5e59adf Merge branch 'perf/lexer-number-scan' into perf/lexer-lazy-float-text
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:23:21 +02:00
Niels Lohmann 547e745289 Merge branch 'claude/parser-stream-performance-8cebe9' into perf/lexer-number-scan
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:23:17 +02:00
Niels Lohmann 1457bfb948 Merge branch 'develop' into claude/parser-stream-performance-8cebe9
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:22:53 +02:00
Niels Lohmann b41228c227 Fix AppVeyor failures from the Zmij double writer (#5843)
MSVC 2015 warns C4554 on the shifts with casts in the fractional part
of zmij::to_shortest despite the parentheses; with /WX every target
fails to build. Name the shift amounts as constants instead.

The shortest-digits test built the candidates with one digit fewer via
a stream with std::setprecision(digits.size() - 2); the standard library
of MSVC 2017 ignores a precision of 0, so for two-digit values the
"shorter" candidates were the value itself. Truncate the digits
instead: the value lies within half a unit of the last digit, so the
truncated digits and their successor are the closest candidates.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:08:12 +02:00
Niels Lohmann dcb96e82fb Escape characters below 0x60 from a table straight into the write buffer (#5822)
* Escape characters below 0x60 from a table straight into the write buffer

When the bulk scanner in dump_escaped_impl() stops at a control
character, a quote or a backslash, that byte used to go through the
UTF-8 decoder and a switch that wrote the escape byte by byte into
string_buffer, which was then copied into write_buffer at the next run.

A byte below 0x60 at a character boundary is a complete code point, so
it now skips the decoder: its escape sequence comes from a 96-entry
table of 8-byte rows (sequence padded with zeros, length in the last
byte), and all 8 bytes are copied straight into write_buffer with one
room check. The check sits inside the existing character-boundary
block, so it runs once per scanner stop and not once per byte; testing
every byte cost about 6% on twitter.json with ensure_ascii.

The switch in the decoder's accept branch lost its cases for
characters below 0x60, which can no longer reach it.

Output is byte-identical. The string-buffer boundary tests now use
\u-escaped non-ASCII characters, since simple escapes no longer reach
string_buffer, and a new test case checks every ASCII character at
every position against a reference escaper.

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

* Avoid raw string literals with backslashes in CHECK macros

MSVC's traditional preprocessor mis-lexes raw string literals inside
macro arguments and reports illegal escape sequences (C2017, C3850).

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

* Sweep invalid UTF-8 at every position through dump()

Compare dump() of strings with ill-formed UTF-8 (stray continuation
bytes, invalid lead bytes, overlong encodings, surrogates, code points
above U+10FFFF, and sequences cut short by ASCII, a quote, a backslash,
a lead byte, or the end of the string) with an independent reference
serializer, for every error handler and with ensure_ascii on and off.
The pattern is put at every position of short strings to cross the
strides of the bulk scanners, and of longer ones to cross the end of the
512-byte string buffer.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:08:10 +02:00
dependabot[bot] c7fd5ac19f Bump the codeql-action group with 4 updates (#5846)
Bumps the codeql-action group with 4 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/autobuild` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/upload-sarif` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 20:05:49 +02:00
dependabot[bot] a678aa6dd7 Bump actions/setup-node from 7.0.0 to 7.1.0 (#5847)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.1.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-10-15 20:05:39 +02:00
dependabot[bot] 3d93f682a7 Bump step-security/harden-runner from 2.22.0 to 2.22.1 (#5848)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.22.0 to 2.22.1.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/351661ca32ac09a36dc5ee2d536e3128f2a3c8ed...ccd8616d44fd3846e67624a50d5aad6d37bf2d25)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 20:05:28 +02:00
dependabot[bot] 1c696dcf05 Bump actions/upload-artifact from 7.0.1 to 7.0.2 (#5849)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.1 to 7.0.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 20:05:17 +02:00
dependabot[bot] 7cd34c62a9 Bump lukka/get-cmake from 4.4.2 to 4.4.4 (#5819)
Bumps [lukka/get-cmake](https://github.com/lukka/get-cmake) from 4.4.2 to 4.4.4.
- [Release notes](https://github.com/lukka/get-cmake/releases)
- [Changelog](https://github.com/lukka/get-cmake/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/lukka/get-cmake/compare/fffaaafeea488556c2c12dad60690008bc1caacb...a89e43e22a96abafcd7a62db652a65ecd33052c9)

---
updated-dependencies:
- dependency-name: lukka/get-cmake
  dependency-version: 4.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 20:05:07 +02:00
Niels Lohmann eb74fe2ab7 Fuzz the streaming input adapters against the contiguous one (#5827)
* Fuzz the streaming input adapters against the contiguous one

The fast paths for contiguous input (bulk string scanning, multi-byte
loads, lazy position tracking) are taken only when parsing from a
pointer range; streams and non-contiguous iterators still read byte by
byte. The fuzzer drivers parsed only contiguous input, so a difference
between the two would ship unnoticed.

check_input_adapters() in fuzzer_common.hpp reads the input through a
std::istringstream, a std::list iterator range, and (on POSIX) a FILE*
from fmemopen, and asserts the same value, or the same exception id,
message and byte position, as for the pointer range. All seven drivers
call it; the JSON driver checks parse and accept, each with and without
ignore_comments. It replaces the stream check of the BON8 driver.

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

* Rename the reader parameter to avoid a flawfinder false positive

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:04:57 +02:00
Niels Lohmann 91b9ee2602 Update cppcheck to 1.5.2 (Cppcheck 2.20) and fix its new findings (#5829)
Cppcheck 2.20 rejects invocations of unknown function-like macros in #if
conditions instead of evaluating them as 0. default_defines.hpp expands
the Hedley macros to the compiler's feature-test operators (__has_attribute
etc.), so define them as 0 in a generated include file.

It also reports a false bufferAccessOutOfBounds in the float overload of
float_from_bits, sizing the parameter as the double overload's; use the
size of the source instead (equal by the static_assert).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:04:46 +02:00
Niels Lohmann ce28c722bc Allow the same type for number_integer_t and number_unsigned_t (#5815)
* Allow the same type for number_integer_t and number_unsigned_t

basic_json did not compile with NumberUnsignedType == NumberIntegerType
because overloads on both types collided. Such a basic_json now stores
every integer as number_integer: the colliding overloads take a
placeholder type, unsigned C++ integers are converted to number_integer_t,
and value_t::number_unsigned maps to number_integer.

Also fixes two problems this configuration would hit: the unsigned
integer parser accepted values that wrap around to negative numbers, and
dump() overflowed when computing the absolute value of the minimum.

A repeated NumberFloatType gets a static_assert.

Refs #2573

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

* Use json::with_integers_t in the test and the docs

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

* ✅ skip patch test on GCC 5 to avoid internal compiler error

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

* Fix CI: name the unsigned overload type through template parameters

basic_json used number_unsigned_overload_t<basic_json> while the class was
still incomplete. MSVC rejected it as a recursive alias declaration and GCC
4.9, 5 and 7 crashed with an internal compiler error. basic_json now uses
number_unsigned_overload_type<NumberIntegerType, NumberUnsignedType>, which
also makes the GCC 5 workaround in the patch test unnecessary.

Infer reported a null dereference for the number_unsigned case in
from_json, which cannot happen for a basic_json whose number_unsigned_t is
number_integer_t; assert it.

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

* Address review: conjunction/negation trait, astyle-friendly alias, docs table order

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:04:42 +02:00
Niels Lohmann 6261d1e432 Construct the JSON Patch exceptions out of line (#5842)
patch_inplace() built its error messages inline. In particular get_value()
concatenated the context-sensitive "operation 'x'" string on every call, even
when nothing was wrong. The patch exceptions now come from NEVER_INLINE
helpers: the index and key errors reuse basic_json's existing
throw_array_index_out_of_range/throw_key_not_found, and the seven
patch-specific messages get one helper each in detail/exceptions.hpp. Ids
and messages are unchanged, and unit-throw_messages pins them (with and
without JSON_DIAGNOSTICS).

citm_catalog patch: -6.6 % (Apple clang), -9.5 % (GCC 16).
The other remaining throw sites (parser::syntax_error, binary_writer,
serializer, input_adapters, enum_key_duplicate, format_error) were prototyped
and measured: no gain beyond noise, so they stay as they are.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:23 +02:00
Niels Lohmann ed86019a21 Move binary reader error construction out of line (#5839)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:23 +02:00
Niels Lohmann 4f11512641 Speed up container destruction, iterator access and diff() (#5835)
* Speed up container destruction, iterator access and diff()

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

* Destroy the first levels of a value by recursion

The pointer-reversing walk in destroy_container() avoids the call stack but
moves a value around for every container it visits. Recurse for the first
recursion_depth_limit() levels instead - no allocation, no thread-local, a
bounded frame - and hand anything deeper to the walk. Destroy twitter
0.49 -> 0.36 ms, citm 0.80 -> 0.74 ms (3.12.0: 0.47 / 1.00).

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:22 +02:00
Niels Lohmann 6a27b9acb2 Construct remaining hot-path exceptions out of line so core functions inline (#5838)
* Construct remaining hot-path exceptions out of line so core functions inline

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

* Move throw_key_not_found() before at_impl()'s comment block

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:22 +02:00
Niels Lohmann 91869983a0 Make the value lifecycle inline-friendly (#5840)
* Make the value lifecycle inline-friendly

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

* Compare standard containers without a thread-local depth count

operator== on objects and arrays recursed through the containers' own
operator== and counted the depth in a thread-local variable. Where reading a
thread-local costs a call (macOS), that added about 10% to comparing documents
with many small containers. For the standard containers, walk objects and
arrays directly, compare scalars and strings inline, and pass the depth as an
argument, falling back to compare_iteratively beyond the depth limit. Other
container types keep the previous path.

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

* Address review: owns_memory() helper, leaf compare naming, astyle

- Add detail::owns_memory(value_t), a switch listing every enumerator so
  -Wswitch-enum flags it when a type is added; destroy() and the copy
  constructor use it instead of hand-written comparisons. Clang emits the
  same bitmask test as before.
- Drop the parentheses around the scalar operands in
  JSON_IMPLEMENT_SCALAR_CASES; they are only needed for the container
  operands (#1530), which the comment now says.
- Build equal_bounded_containers from detail::conjunction so astyle no
  longer pads the template brackets.
- Rename equal_scalar() to equal_leaf() and document why it also compares
  strings, unlike JSON_IMPLEMENT_SCALAR_FAST.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:21 +02:00
Niels Lohmann 4530d9681c Fix quadratic parent updates for ordered_json with JSON_DIAGNOSTICS (#5832)
* Fix quadratic parent updates for ordered_json with JSON_DIAGNOSTICS

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

* Only trust containers known to keep their elements in place

Address review: decide whether an insertion may have moved the other
members by naming the containers that are known not to, instead of the
ones that are known to.

- detail::has_stable_elements: std::map, std::multimap,
  std::unordered_map and std::unordered_multimap; object_capacity()
  reports 0 for them.
- detail::is_nlohmann_ordered_map: only the real nlohmann::ordered_map
  (which only appends) reports its capacity().
- Any other object type reports unknown_size(), so it gets a full
  set_parents() on every insertion. Before, an object type without
  capacity() was treated as never moving, and one with capacity() (a
  sorted flat map, say) was trusted to move only on reallocation.
- set_parents_after_object_erase() re-parents unless the object type is
  stable; the unused set_parent() is removed.
- Comments say "the object type" instead of "ordered_json".
- New test with an object type that moves its members without changing
  its capacity; it fails without this change.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:21 +02:00
Niels Lohmann ca7740fef2 Move elements when converting rvalue containers to JSON (#5837)
* Move elements when converting rvalue containers to JSON

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

* Simplify the rvalue to_json traits

Use negation<disjunction<...>> instead of a conjunction of negations and
keep the template argument lists compact (astyle only keeps the spaced
'< ... >' form when it is written that way).

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:20 +02:00
Niels Lohmann 6ba53e63dd Speed up array-to-container from_json (#5833)
* Speed up array-to-container from_json

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

* Hint the end when converting an object to a standard container

from_json for map-like containers called emplace() without a hint, which
searches the tree for every key. The keys of the source object are sorted, so
inserting with the end as the hint is constant time, as it was in 3.12.0 via
std::inserter. get<std::map<...>>() of a 25,000 entry object: 4.2 ms -> 3.25 ms
(3.12.0: 3.7 ms).

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

* Fall back to emplace() in object from_json for containers without emplace_hint()

ordered_map has no hinted insert, so the insert(end(), ...) call broke
from_json into types holding an ordered_map (regression test #5122).
Use emplace_hint() where available and plain emplace() otherwise.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:20 +02:00
Niels Lohmann 0a4b3a84ee Speed up JSON pointer parsing, formatting, comparison and lookup (#5836)
* Speed up JSON pointer parsing, formatting, comparison and lookup

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

* Restore unflatten() speed after the linear-time rewrite

The prefix tree introduced for linear unflatten() kept a std::map per prefix,
copied each token into it and looked every token up a second time while
creating the values. Replace the per-prefix maps by one open-addressing table
keyed by (parent, token) that refers to the parsed tokens, skip the lookups for
the prefix shared with the previous key (flattened objects are mostly sorted),
and derive the prefix numbers of the second pass from parent links instead of
lookups.

Time and memory stay linear in the pointer depth, and the result is still
independent of the iteration order of the flattened object.

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

* Put [[noreturn]] before __attribute__ on the JSON pointer throw helpers

Clang 18 on Linux rejects a C++11 attribute list after a GNU
__attribute__ ("an attribute list cannot appear here").

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:20 +02:00
Niels Lohmann 0c4e7c103c Make flatten() linear for ordered_json (#5834)
* Make flatten() linear for ordered_json

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

* Use priority_tag instead of a C-style variadic overload in add_flattened

clang-tidy flags the ellipsis fallback (cert-dcl50-cpp,
modernize-avoid-variadic-functions).

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:19 +02:00
Niels Lohmann a6e9785ab6 Look up the key only once in the callback parser's key() (#5831)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 20:01:19 +02:00
Niels Lohmann c6742f9761 Scan strings with SSE2, SSSE3, or NEON
The string fast path found the end of a run of plain characters 8 bytes at
a time (SWAR) and validated non-ASCII text one sequence at a time, calling
validate_one_utf8() out of line for each.

The vector code of json_view's scanner, in the new shared header
detail/input/simd_scan.hpp, now does both 16 bytes at a time: SSE2 finds
the end of a run on x86-64, and the UTF-8 check of Keiser and Lemire runs
with NEON on AArch64 and with SSSE3 on x86-64. SSSE3 is not part of x86-64,
so the check is compiled for it with a function attribute and used when the
CPU has it (cached in a constant-initialized atomic), else the scalar check;
code compiled with -mssse3 skips the CPU check. The first word of a run is
still checked with SWAR (the first four with NEON, where a vector mask
costs more), as most keys and short values end there, and the scalar
helpers are always inlined. On anything else, the scalar code is unchanged.

The new macro JSON_NO_SIMD selects the portable code everywhere, including
the SSE2/NEON digit formatting of the floating-point writer. The lexer tests
now also run with JSON_NO_SIMD and, on x86-64, with -mssse3, and a new test
places every byte that ends a run at each offset across several 16-byte
blocks, after ASCII and after CJK text.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 18:37:04 +02:00
Niels Lohmann 5423dd8de3 Copy the text of float tokens only when a SAX handler asks for it
After the digits are converted while scanning, copying the token text into
token_buffer (an out-of-line std::string::append) was about 10% of parsing
canada.json. The only reader is the string argument of
json_sax::number_float(), and the library's own handlers
(json_sax_dom_parser, json_sax_dom_callback_parser, json_sax_acceptor)
ignore it.

For contiguous input, the fast path now keeps a pointer to the token in the
input, and get_string() copies it on first use. The parser passes an empty
string to the handlers marked by the new internal trait
sax_ignores_number_text instead of calling get_string(), so json::parse()
and json::accept() never copy float text. User handlers, and
JSON_DIAGNOSTIC_POSITIONS (which reads get_string().size()), still see the
exact text. Keys and strings, which never have pending text, use the
unchecked get_string_token(). Input read through a window into a stream
(std::istream, FILE*) copies the text right away, because the next read may
refill the window.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 18:37:02 +02:00
Niels Lohmann 32ca00cdfa Accumulate number digits while scanning them
The contiguous number fast path validated a token byte by byte, copied it
into token_buffer, and then converted it in out-of-line functions that
scanned the digits a second time: parse_float_native() called
parse_float_significand(), which returned its result through memory and
called std::any_of (out of line with GCC) twice per number, and
parse_integer_signed() divided by ten on every digit for its overflow check.

scan_number_bulk_contiguous() now accumulates the significand while it
validates the digits, eight at a time where at least eight bytes remain
(is_eight_digits() and parse_eight_digits(), after fast_float). Floats with
at most 19 digits go straight to decimal_to_float(); integers with at most
18 digits are stored without an overflow check (a narrower number type
still rejects values that do not round-trip). Longer tokens take the
existing path. has_nonzero_digit() is a plain loop and
parse_float_significand() is always inlined, which helps that path.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 18:37:01 +02:00
Niels Lohmann 598fbd0417 Avoid implicit unsigned char to char conversion in stream tests
Building a std::string from the iterators of a CBOR byte vector narrows
each byte, which -fsanitize=implicit-conversion reports for bytes above
0x7F. Construct the string from the raw bytes instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 21:30:07 +02:00
Niels Lohmann 898084b386 Merge branch 'develop' into claude/parser-stream-performance-8cebe9
Resolve the macro_unscope.hpp conflict by keeping both sets of #undefs
and regenerate the amalgamation.

The \u fast path from #5738 (get_codepoint_bulk) assumed that bulk
scanning implies lazy position tracking and advanced the adapter with a
bare bulk_skip(4). Stream adapters now scan in bulk but track position
and token eagerly, so a lone-surrogate error was reported 4 columns too
early. Advance through bulk_consume() instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 21:30:06 +02:00
Niels Lohmann 4e294f4b18 Clarify that double is only the default floating-point type in the docs (#5830)
Follow-up to review comments on #5825: number_float_t uses double
precision only by default, and the stated limits apply to double.
Also say "default type for X_t" instead of "default value for X_t".

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 18:28:49 +02:00
Niels Lohmann f13fbd2577 Add benchmarks for parsing from a FILE* and for many values from a stream
ParseFILE/* parses the nativejson-benchmark files from a FILE*, and
ParseStreamValues/* reads 10,000 small values from one std::istringstream
with operator>> (NDJSON objects and plain numbers), which measures the
per-value cost of stream input.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:34:01 +02:00
Niels Lohmann c391abccb1 Lock a FILE once per parse instead of once per character
Parsing from a FILE* reads one character at a time with std::fgetc, which
locks and unlocks the FILE object for every character. file_input_adapter
now locks the file once when it is created and unlocks it when it is
destroyed - also when parsing throws - and reads with the unlocked stdio
functions in between: flockfile/getc_unlocked/funlockfile with glibc,
Apple, Android, and the BSDs, and _lock_file/_fgetc_nolock/_unlock_file
with the Microsoft C runtime. Other C libraries keep std::fgetc. The lock
is recursive, so fread() in the binary readers and stdio calls of a SAX
handler or parser callback on the same thread keep working.

On an Apple M1 Max, parsing from a FILE* is 2.9 to 4.0 times as fast
(twitter.json 14.3 -> 4.1 ms, citm_catalog.json 35.4 -> 8.8 ms); with
glibc, whose locking is cheaper, 1.1 to 1.3 times. The values and the
position of the file after parsing are unchanged.

Other threads that use the same FILE object now wait until parsing is
done instead of interleaving their reads with the parser's.
JSON_DISABLE_UNLOCKED_FILE_IO restores std::fgetc, for programs in which a
SAX handler waits for another thread that uses the same FILE. The new
tests check from a second thread that the lock is released after success
and after a parse error, and run again with the macro.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:34:00 +02:00
Niels Lohmann d5631dccbd Scan the get area of an input stream in bulk
Parsing from a std::istream read one character at a time through the
streambuf, so none of the parser's fast paths applied: it was 1.8 to 2.5
times slower than reading the file into a string and parsing that.

input_stream_adapter now exposes the streambuf's get area (gptr() to
egptr()) to the lexer as a window. The protected get-area members are
reached through member pointers formed in a class derived from
std::streambuf, which is ordinary, standard-conforming member access. The
lexer scans the window with the same bulk paths as a string, and asks
the streambuf for the next character as before when the window is used
up. Nothing is read ahead, copied, or put back, so the stream is left
exactly where it was left before, with and without
JSON_PRECISE_STREAM_POSITION, and a streambuf without a get area is still
read one character at a time.

The adapter also keeps the current token for error messages as a pointer
into the get area, and copies it out only before the get area is
refilled, instead of the lexer copying every character.

On an Apple M1 Max (Apple Clang, -O3), parsing from an std::ifstream is
1.6 to 2.1 times as fast (twitter.json 4.08 -> 2.30 ms, citm_catalog.json
8.75 -> 4.08 ms), and reading 100,000 small values with operator>> 1.4
(objects) to 4.0 (integers) times as fast. With GCC 13 and libstdc++ on
x86-64 Linux, std::ifstream is 1.5 to 2.2 times as fast. Parsing from a
stream is now within 11 to 18 % of reading the file into a string first.

JSON_DISABLE_STREAM_BULK_SCAN restores reading one character at a time,
for streambufs that do not follow the usual get-area contract. The new
tests compare every observable result (values, exception messages, stream
state, remaining characters) for get areas of 1 to 4096 bytes against a
streambuf without a get area, and run again with the macro and with
JSON_PRECISE_STREAM_POSITION.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:26:38 +02:00
Niels Lohmann 80d00eda50 Let the lexer's bulk fast paths run on a window of the input
The SWAR string scan, the bulk number scan, and the indentation skip were
only enabled for adapters that support seeking (contiguous random-access
input), because they rely on the lazy token and position tracking that
reconstructs both from the consumed input on demand.

This decouples the bulk paths from seeking, so that an adapter can expose
a contiguous block that is only a window into its input (bulk_window):

- bulk_consume() does the bookkeeping that get() would have done for the
  skipped characters when token and position are tracked eagerly.
- A number that reaches the end of a window is finished character by
  character, because it may continue after the window.
- After a bulk-scanned number, a window adapter reads the terminating
  character and ungets it, as scan_number() does, so that the input
  handed back to the user is consumed exactly as before.
- An adapter can keep the current token itself (supports_token_tracking,
  begin_token(), mark_token_start(), copy_token()) so the lexer need not
  copy every character for its error messages.

No adapter declares any of this yet, so the behavior is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-14 17:13:58 +02:00
98 changed files with 9871 additions and 1768 deletions

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+4 -4
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -31,7 +31,7 @@ jobs:
mkdir -p ./pr
echo ${{ github.event.number }} > ./pr/number
echo ${{ github.event.pull_request.user.login }} > ./pr/author
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: pr
path: pr/
@@ -46,7 +46,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -170,7 +170,7 @@ jobs:
# instead of installing the pinned astyle version locally.
- name: Upload patch
if: steps.diff.outputs.has_diff == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: amalgamation-patch
path: patch/amalgamation.patch
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -35,7 +35,7 @@ jobs:
dry-run: false
language: c++
- name: Upload Crash
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -38,14 +38,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: c-cpp
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
@@ -20,7 +20,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+2 -2
View File
@@ -31,7 +31,7 @@ jobs:
security-events: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -47,6 +47,6 @@ jobs:
output: 'flawfinder_results.sarif'
- name: Upload analysis results to GitHub Security tab
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: ${{github.workspace}}/flawfinder_results.sarif
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -72,7 +72,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: SARIF file
path: results.sarif
@@ -80,6 +80,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: results.sarif
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -65,7 +65,7 @@ jobs:
# Upload SARIF file generated in previous step
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: semgrep.sarif
if: always()
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+26 -26
View File
@@ -25,7 +25,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -38,7 +38,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Check that Meson and pkg-config offer the CMake options
run: make check_build_options
- name: Install Meson
@@ -80,7 +80,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -92,14 +92,14 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_infer
- name: Archive Infer report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: infer-report
path: ${{ github.workspace }}/build/build_infer/infer-out/report.txt
@@ -112,7 +112,7 @@ jobs:
target: [ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_reuse_compliance, ci_test_valgrind]
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -123,7 +123,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -142,7 +142,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -168,7 +168,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -178,7 +178,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -197,7 +197,7 @@ jobs:
- name: Build
run: cmake --build build --target ci_test_coverage
- name: Archive coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: code-coverage-report
path: ${{ github.workspace }}/build/html
@@ -244,7 +244,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: CXX=g++-${{ matrix.compiler }} cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -262,7 +262,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -279,7 +279,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Disable filesystem support (std::filesystem is unusable) for clang 7 / 8 / 9 / 10
run: echo "JSON_FORCED_GLOBAL_COMPILE_OPTIONS=-DJSON_HAS_FILESYSTEM=0;-DJSON_HAS_EXPERIMENTAL_FILESYSTEM=0" >> "$GITHUB_ENV"
if: ${{ matrix.compiler == '7' || matrix.compiler == '8' || matrix.compiler == '9' || matrix.compiler == '10' }}
@@ -299,7 +299,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -319,7 +319,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build with libc++
@@ -346,7 +346,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -366,7 +366,7 @@ jobs:
# import-std support. Its opt-in token is CMake-version-specific, so pin
# CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt.
- name: Get pinned CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
with:
cmakeVersion: 4.3.4
# Clang: the std library module is provided by libc++ (the image's libstdc++
@@ -392,7 +392,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -409,7 +409,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -423,7 +423,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
@@ -433,7 +433,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -443,7 +443,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -GNinja
- name: Build
@@ -456,7 +456,7 @@ jobs:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -465,7 +465,7 @@ jobs:
persist-credentials: false
# the git-revision-date-localized plugin needs the history; a shallow clone makes the strict build fail
fetch-depth: ${{ matrix.target == 'ci_test_build_documentation' && '0' || '1' }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
@@ -484,7 +484,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Check that the job runs natively on arm64
run: uname -m && test "$(uname -m)" = aarch64
- name: Run CMake
+2 -2
View File
@@ -90,7 +90,7 @@ jobs:
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Set extra CXX_FLAGS for latest std_version
# /wd5285 silences C5285 emitted by the bundled third-party doctest.h, which
# specializes std::tuple (newly diagnosed by the VS2026 v145 toolset)
@@ -214,7 +214,7 @@ jobs:
# import-std support. Its opt-in token is CMake-version-specific, so pin
# CMake to the version whose token is set in tests/module_cpp20/CMakeLists.txt.
- name: Get pinned CMake and ninja
uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
with:
cmakeVersion: 4.3.4
# CMAKE_CXX_FLAGS replaces CMake's MSVC defaults (which include /EHsc), and
+1
View File
@@ -36,6 +36,7 @@ cc_library(
"include/nlohmann/detail/input/parser.hpp",
"include/nlohmann/detail/input/position_t.hpp",
"include/nlohmann/detail/input/pow5_table.hpp",
"include/nlohmann/detail/input/simd_scan.hpp",
"include/nlohmann/detail/input/string_scan.hpp",
"include/nlohmann/detail/iterators/internal_iterator.hpp",
"include/nlohmann/detail/iterators/iter_impl.hpp",
+8 -1
View File
@@ -485,6 +485,13 @@ add_custom_target(ci_clang_analyze
# Check code with Cppcheck.
###############################################################################
# default_defines.hpp expands the Hedley macros to the compiler's feature-test operators, which Cppcheck does not know.
# Since Cppcheck 2.20, it rejects their invocations in #if conditions instead of evaluating them as 0.
file(WRITE ${CMAKE_BINARY_DIR}/cppcheck_feature_tests.hpp "")
foreach(OPERATOR __has_attribute __has_builtin __has_cpp_attribute __has_declspec_attribute __has_extension __has_feature __has_include __has_warning)
file(APPEND ${CMAKE_BINARY_DIR}/cppcheck_feature_tests.hpp "#define ${OPERATOR}(x) 0\n")
endforeach()
add_custom_target(ci_cppcheck
COMMAND mkdir -p cppcheck
COMMAND clang -dM -E -x c++ -std=c++11 ${CMAKE_SOURCE_DIR}/include/nlohmann/thirdparty/hedley/hedley.hpp > default_defines.hpp 2> /dev/null
@@ -493,7 +500,7 @@ add_custom_target(ci_cppcheck
COMMAND venv_cppcheck/bin/cppcheck --enable=warning --check-level=exhaustive --inline-suppr --inconclusive --force
--std=c++11 ${PROJECT_SOURCE_DIR}/include/nlohmann/json.hpp -I ${CMAKE_SOURCE_DIR}/include
--error-exitcode=1 --relative-paths=${PROJECT_SOURCE_DIR} -j ${N} --include=default_defines.hpp
--cppcheck-build-dir=cppcheck
--include=${CMAKE_BINARY_DIR}/cppcheck_feature_tests.hpp --cppcheck-build-dir=cppcheck
-UJSON_CATCH_USER -UJSON_TRY_USER -UJSON_ASSERT -UJSON_INTERNAL_CATCH -UJSON_THROW
-DJSON_HAS_CPP_11 -UJSON_HAS_CPP_14 -UJSON_HAS_CPP_17 -UJSON_HAS_CPP_20 -UJSON_HAS_THREE_WAY_COMPARISON
COMMENT "Check code with Cppcheck"
+1 -1
View File
@@ -1 +1 @@
cppcheck==1.5.1
cppcheck==1.5.2
@@ -132,6 +132,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- [operator>>](../operator_gtgt.md) - deserialize from stream
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -143,6 +147,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_STRICT_NUL_HANDLING` added in version 3.13.0 to optionally reject a NUL byte in the input instead of treating
it as end of input; planned to become the default in version 4.0.0.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
!!! warning "Deprecation"
+1 -1
View File
@@ -27,7 +27,7 @@ To store objects in C++, a type is defined by the template parameters explained
### Default type
With the default values for `ArrayType` (`std::vector`) and `AllocatorType` (`std::allocator`), the default value for
With the default values for `ArrayType` (`std::vector`) and `AllocatorType` (`std::allocator`), the default type for
`array_t` is:
```cpp
+11 -1
View File
@@ -68,6 +68,14 @@ basic_json(basic_json&& other) noexcept;
`std::unordered_map`, `std::multimap`, and `std::unordered_multimap` with a `key_type` compatible to `string_t`
and a `value_type` from which a `basic_json` value can be constructed.
- **strings**: `string_t`, string literals, and all compatible string containers can be used.
!!! note "Rvalue containers are moved from"
If `val` is an rvalue of a compatible array-like or object-like container other than `array_t`/`object_t`
(e.g., `json j = std::move(vec);` or `json j(std::vector<std::string>{...});`), the elements are moved into
the JSON value instead of copied. Afterwards, the elements of the source container are in a valid but
unspecified state (**moved-from**). Keys of associative containers are `const` and therefore still copied.
Lvalues and `const` rvalues are copied, and the source is left unchanged.
- **numbers**: [`number_integer_t`](number_integer_t.md), [`number_unsigned_t`](number_unsigned_t.md),
[`number_float_t`](number_float_t.md), and all convertible number types such as `int`, `size_t`, `int64_t`, `float`
or `double` can be used.
@@ -236,7 +244,9 @@ basic_json(basic_json&& other) noexcept;
2. No-throw guarantee: this constructor never throws exceptions.
3. Depends on the called constructor. For types directly supported by the library (i.e., all types for which no
`to_json()` function was provided), a strong guarantee holds: if an exception is thrown, there are no changes to any
JSON value.
JSON value. **Exception:** when `val` is an rvalue array-like or object-like container, its elements are moved one
by one; if an exception is thrown in the middle of the conversion (e.g., an allocation failure), the JSON value is
unchanged, but the source container may be left partially moved-from.
4. Depends on the called constructor. For types directly supported by the library (i.e., all types for which no
`to_json()` function was provided), a strong guarantee holds: if an exception is thrown, there are no changes to any
JSON value.
+1 -1
View File
@@ -23,7 +23,7 @@ To store boolean values in C++, a type is defined by the template parameter `Bo
### Default type
With the default values for `BooleanType` (`#!cpp bool`), the default value for `boolean_t` is `#!cpp bool`.
With the default values for `BooleanType` (`#!cpp bool`), the default type for `boolean_t` is `#!cpp bool`.
### Storage
@@ -6,6 +6,9 @@ constexpr bool is_number_unsigned() const noexcept;
This function returns `#!cpp true` if and only if the JSON value is an unsigned integer number. This excludes
floating-point and signed integer values.
If [`number_unsigned_t`](number_unsigned_t.md) is the same type as [`number_integer_t`](number_integer_t.md), every
integer is stored as `number_integer_t`, and this function always returns `#!cpp false`.
## Return value
@@ -35,7 +35,7 @@ type to use.
### Default type
With the default values for `NumberFloatType` (`double`), the default value for `number_float_t` is `#!cpp double`.
With the default values for `NumberFloatType` (`double`), the default type for `number_float_t` is `#!cpp double`.
### Default behavior
@@ -52,8 +52,9 @@ With the default values for `NumberFloatType` (`double`), the default value for
> interoperability can be achieved by implementations that expect no more precision or range than these provide, in the
> sense that implementations will approximate JSON numbers within the expected precision.
This implementation does exactly follow this approach, as it uses double precision floating-point numbers. Note that
numbers smaller than `-1.79769313486232e+308` or greater than `1.79769313486232e+308` are rejected during parsing with
This implementation does exactly follow this approach, as it uses double precision floating-point numbers by default.
Note that with the default `double`, numbers smaller than `-1.79769313486232e+308` or greater than
`1.79769313486232e+308` are rejected during parsing with
[`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406). Non-finite values (NaN and infinity) can
only be stored by assigning them directly and are serialized to `null`.
@@ -31,7 +31,7 @@ to use.
### Default type
With the default values for `NumberIntegerType` (`std::int64_t`), the default value for `number_integer_t` is
With the default values for `NumberIntegerType` (`std::int64_t`), the default type for `number_integer_t` is
`#!cpp std::int64_t`.
### Default behavior
@@ -25,14 +25,15 @@ the type to use.
`NumberUnsignedType`
: the type to store unsigned integers. It must be an **unsigned integral** type (`#!cpp std::is_integral`) with a
`#!cpp std::numeric_limits` specialization, and it must be able to represent the absolute value of every
[`number_integer_t`](number_integer_t.md) value. See
[`number_integer_t`](number_integer_t.md) value. Alternatively, it can be the same type as
[`number_integer_t`](number_integer_t.md); then every integer is stored as `number_integer_t`. See
[Template Parameter Requirements](../../features/types/template_parameters.md#numberintegertype-and-numberunsignedtype).
## Notes
### Default type
With the default values for `NumberUnsignedType` (`std::uint64_t`), the default value for `number_unsigned_t` is
With the default values for `NumberUnsignedType` (`std::uint64_t`), the default type for `number_unsigned_t` is
`#!cpp std::uint64_t`.
### Default behavior
@@ -89,3 +90,4 @@ Integer number values are stored directly inside a `basic_json` type.
## Version history
- Added in version 2.0.0.
- Can be the same type as `number_integer_t` since version 3.13.0.
+1 -1
View File
@@ -36,7 +36,7 @@ To store objects in C++, a type is defined by the template parameters described
### Default type
With the default values for `ObjectType` (`std::map`), `StringType` (`std::string`), and `AllocatorType`
(`std::allocator`), the default value for `object_t` is:
(`std::allocator`), the default type for `object_t` is:
```cpp
// until C++14
+19
View File
@@ -112,6 +112,16 @@ By default, a `'\0'` (NUL) byte anywhere in the input is treated as end of input
outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-bytes-in-the-input) for details and the
[`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) macro to opt into rejecting it instead.
!!! tip "Performance"
Contiguous inputs (strings, string views, vectors, arrays, pointer ranges) are the fastest to parse. A
`#!cpp std::istream` comes close: the parser scans the characters its stream buffer holds in bulk, without reading
ahead, so the stream is left exactly where it would be after reading one character at a time. A `#!cpp FILE*` is
read one character at a time, but the file is locked only once, so other threads that use the same `FILE` object
wait until parsing is done. See [input sources and performance](../../features/parsing/index.md#input-sources-and-performance),
[`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md), and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
## Examples
??? example "Example: (1) parse from a character array"
@@ -247,6 +257,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- [operator>>](../operator_gtgt.md) - deserialize from stream
- [`JSON_STRICT_NUL_HANDLING`](../macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -261,6 +275,11 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
it as end of input; planned to become the default in version 4.0.0.
- Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash
(`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) restores reading one character at a
time).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) restores `std::fgetc`).
!!! warning "Deprecation"
@@ -116,6 +116,10 @@ consumer `sax` has a super-linear complexity.
A UTF-8 byte order mark is silently ignored.
When reading from a `#!c FILE*`, the file is locked while it is parsed. The lock is recursive, so the SAX handler may
use the same file with stdio functions, but it must not wait for another thread that uses the same `FILE` object; see
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md).
## Examples
??? example
@@ -137,6 +141,10 @@ A UTF-8 byte order mark is silently ignored.
- [parse](parse.md) - deserialize from a compatible input
- [accept](accept.md) - check if the input is valid JSON
- [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) - read a `std::istream` one character
at a time
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
## Version history
@@ -148,6 +156,10 @@ A UTF-8 byte order mark is silently ignored.
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave a `#!cpp std::istream` positioned right
after the parsed value when `strict` is `#!cpp false`.
- Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0
([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md)).
- Parsing from a `FILE*` locks the file once instead of once per character in version 3.13.0
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md)).
!!! warning "Deprecation"
+1 -1
View File
@@ -32,7 +32,7 @@ JSON class into byte-sized characters during deserialization.
### Default type
With the default values for `StringType` (`std::string`), the default value for `string_t` is `#!cpp std::string`.
With the default values for `StringType` (`std::string`), the default type for `string_t` is `#!cpp std::string`.
### Encoding
+5
View File
@@ -16,6 +16,10 @@ header. See also the [macro overview page](../../features/macros.md).
## Parsing
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
instead of scanning its stream buffer in bulk
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with `std::fgetc` instead of
locking it once and using the unlocked stdio functions
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - opt in to leaving an input stream positioned
right after a parsed number
- [**JSON_STRICT_BINARY_UTF8**](json_strict_binary_utf8.md) - opt in to checking strings for valid UTF-8 in the CBOR,
@@ -34,6 +38,7 @@ header. See also the [macro overview page](../../features/macros.md).
- [**JSON_HAS_THREE_WAY_COMPARISON**](json_has_three_way_comparison.md) - control 3-way comparison support
- [**JSON_NO_AUTOMATIC_UDLS**](json_no_automatic_udls.md) - do not include the user-defined string literals (UDLs) automatically
- [**JSON_NO_IO**](json_no_io.md) - switch off functions relying on certain C++ I/O headers
- [**JSON_NO_SIMD**](json_no_simd.md) - use only portable code instead of vector instructions (SSE2, SSSE3, NEON)
- [**JSON_NO_THREAD_LOCAL**](json_no_thread_local.md) - switch off the use of `thread_local` storage
- [**JSON_SKIP_UNSUPPORTED_COMPILER_CHECK**](json_skip_unsupported_compiler_check.md) - do not warn about unsupported compilers
- [**JSON_USE_GLOBAL_UDLS**](json_use_global_udls.md) - place user-defined string literals (UDLs) into the global namespace
@@ -0,0 +1,145 @@
# JSON_DISABLE_STREAM_BULK_SCAN
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN /* value */
```
When defined to `1`, parsing from a `#!cpp std::istream` reads the input one character at a time through the stream
buffer, as in version 3.12.0 and earlier, instead of scanning the characters that the stream buffer already holds in
bulk.
The macro is an escape hatch for stream buffers that do not follow the usual contract of `std::basic_streambuf` (see
the requirements on the stream buffer in the notes below). It does not change any result: values, exceptions, and the
position of the stream after parsing are the same either way. Only the speed differs.
## Default definition
The default value is `0`: streams are scanned in bulk.
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN 0
```
## Notes
!!! note "How streams are scanned"
A `std::streambuf` keeps the part of the input it has read but not yet handed out in its *get area*, the range
from `gptr()` to `egptr()`. A `std::filebuf`, for instance, reads the file in blocks of a few kilobytes into its get
area, and a `std::stringbuf` exposes its whole string as one get area.
When parsing from a `std::istream`, the parser scans this get area directly, with the same fast paths that it uses
for a `std::string`: runs of ordinary string characters, numbers, and indentation are recognized several bytes at a
time instead of being requested from the stream buffer one character at a time. When the parser reaches the end of
the get area, it asks the stream buffer for the next character as before, which makes the stream buffer refill its
get area.
The parser only reads characters that it would also have read one at a time, and it advances the stream buffer
exactly over the characters it consumes. Nothing is read ahead, buffered elsewhere, or put back. Therefore, the
stream is left exactly where it was left before:
- [`operator>>`](../operator_gtgt.md) and [`sax_parse`](../basic_json/sax_parse.md) with `strict = false` can still
read several values from one stream, and the stream can still be used with other `std::istream` functions after a
value has been read.
- With [`JSON_PRECISE_STREAM_POSITION`](json_precise_stream_position.md), the stream is still positioned right after
a parsed number.
- Exception messages, including the line, the column, and the last read token, are unchanged.
!!! note "Performance"
Parsing from a `std::ifstream` or `std::istringstream` is typically 1.5 to 2 times as fast as before, and reading
many small values from one stream with `operator>>` 1.4 to 4 times as fast. Parsing from a stream is then within
about 10 to 20 % of first reading the whole file into a `std::string` and parsing that.
A stream buffer without a get area (one that only overrides `underflow()` and `uflow()` and hands out every
character from there) offers nothing to scan in bulk, so it is read one character at a time as before.
!!! note "Requirements on the stream buffer"
The bulk scan relies on the following behavior, which every stream buffer of the standard library and every stream
buffer that keeps its input in its get area has:
- When `sgetc()` returns a character while `gptr() < egptr()`, that character is `*gptr()`; when `sbumpc()` returns
one, it was at `gptr()` before the call.
- Characters in the get area are not changed while they are in the get area.
- `underflow()` or `uflow()` either sets up a get area that holds the returned character at `gptr()` (or at
`gptr() - 1` after `uflow()`), or leaves the get area empty.
A stream buffer that returns characters from `uflow()` while leaving unrelated characters in its get area, or that
changes the characters in its get area in place, does not meet these requirements. Define the macro to `1` for
such stream buffers.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal stream input adapter. It must therefore be
defined identically for **every** translation unit that includes the library; mixing translation units that define
it with ones that do not is an ODR violation. Prefer setting it as a compile definition on the target rather than
with `#!cpp #define` in individual source files.
!!! note "Other inputs"
Strings, string views, vectors, arrays, and pointer or iterator ranges over contiguous memory are scanned in bulk
anyway. Reading from a `#!cpp FILE*` is not affected by this macro; see
[`JSON_DISABLE_UNLOCKED_FILE_IO`](json_disable_unlocked_file_io.md) for that input.
## Examples
??? example "Example: reading values from a stream (default)"
The stream is scanned in bulk, and the values are read one after the other:
```cpp
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
json j1, j2, j3;
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
std::string word;
input >> word; // "rest"
}
```
??? example "Example: reading one character at a time (macro defined to 1)"
With the macro, the same program reads the stream one character at a time, with the same results:
```cpp
#define JSON_DISABLE_STREAM_BULK_SCAN 1
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::istringstream input(R"({"id": 1} {"id": 2} 42 rest)");
json j1, j2, j3;
input >> j1 >> j2 >> j3; // {"id":1}, {"id":2}, 42
std::string word;
input >> word; // "rest"
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**operator>>**](../operator_gtgt.md) - deserialize from stream
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
- [**JSON_PRECISE_STREAM_POSITION**](json_precise_stream_position.md) - leave an input stream positioned right after a
parsed number
- [**JSON_DISABLE_UNLOCKED_FILE_IO**](json_disable_unlocked_file_io.md) - read a `FILE*` with the locking stdio
functions
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -0,0 +1,113 @@
# JSON_DISABLE_UNLOCKED_FILE_IO
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO /* value */
```
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, as in version 3.12.0 and
earlier, instead of locking the file once and reading it with the unlocked stdio functions.
It does not change any result: values, exceptions, and the position of the file after parsing are the same either way.
Only the speed and the locking of the file differ (see the notes on locking below).
## Default definition
The default value is `0`: the file is locked once and read with the unlocked stdio functions where they are available.
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
```
## Notes
!!! note "Background"
The C standard library makes every `std::fgetc` call thread-safe by locking and unlocking the `FILE` object. When
parsing from a `FILE*`, the parser reads one character at a time, so this happens for every character of the input.
Instead, the parser locks the file once when parsing starts and unlocks it when parsing ends - also when an
exception is thrown - and reads the characters in between without locking:
| C library | functions |
|------------------------------------------------------------|----------------------------------------------------|
| glibc (Linux), Apple (macOS, iOS), Android, FreeBSD, NetBSD, OpenBSD, DragonFly BSD | `flockfile`, `getc_unlocked`, `funlockfile` (POSIX) |
| Microsoft (MSVC, clang-cl) | `_lock_file`, `_fgetc_nolock`, `_unlock_file` |
| others (e.g., musl, MinGW, newlib) | `std::fgetc`, as before |
This makes parsing from a `FILE*` 3 to 4 times as fast with the Apple C library and 1.1 to 1.3 times as fast with
glibc, whose locking is cheaper. The binary formats ([`from_cbor`](../basic_json/from_cbor.md),
[`from_msgpack`](../basic_json/from_msgpack.md), ...) read from a `FILE*` the same way.
!!! note "Locking"
While a value is parsed from a `FILE*`, the parsing thread holds the file's lock. The lock is recursive, so the
parsing thread itself - for instance, a [SAX](../json_sax/index.md) handler or a
[parser callback](../basic_json/parser_callback_t.md) - can keep using the file with stdio functions such as
`std::ftell`. Other threads that use the same `FILE` object wait until the parser is done. Before, they could
interleave their reads with the parser's, which made the parsed input unpredictable anyway.
A program in which a SAX handler or parser callback waits for **another** thread that uses the **same** `FILE`
object would deadlock. Define the macro to `1` for such programs.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal `FILE*` input adapter. It must therefore be
defined identically for **every** translation unit that includes the library; mixing translation units that define
it with ones that do not is an ODR violation (and can leave a file locked). Prefer setting it as a compile
definition on the target rather than with `#!cpp #define` in individual source files.
!!! note "Other inputs"
Reading from a `#!cpp std::istream` is not affected by this macro; see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md) for that input.
## Examples
??? example "Example: parsing from a `FILE*` (default)"
The file is locked once while it is parsed:
```cpp
#include <cstdio>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::FILE* f = std::fopen("example.json", "rb");
json j = json::parse(f); // f is locked until parse() returns
std::fclose(f);
}
```
??? example "Example: locking for every character (macro defined to 1)"
With the macro, every character is read with `std::fgetc`, with the same result:
```cpp
#define JSON_DISABLE_UNLOCKED_FILE_IO 1
#include <cstdio>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
std::FILE* f = std::fopen("example.json", "rb");
json j = json::parse(f); // std::fgetc for every character
std::fclose(f);
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**sax_parse**](../basic_json/sax_parse.md) - generate SAX events
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a
time
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -0,0 +1,83 @@
# JSON_NO_SIMD
```cpp
#define JSON_NO_SIMD
```
When defined, the library uses only portable C++ code where it would otherwise use vector instructions: when scanning
strings during parsing, and when writing floating-point numbers during serialization.
The macro is an escape hatch, for example for toolchains that cannot compile the vector code, for sanitizers or
analyzers that do not understand it, or to compare the speed of both variants. It does not change any result: values,
exceptions, and error positions are the same either way. Only the speed differs.
## Default definition
By default, `#!cpp JSON_NO_SIMD` is not defined, and vector instructions are used where the target has them.
```cpp
#undef JSON_NO_SIMD
```
## Notes
!!! note "Which instructions are used"
- **x86-64:** SSE2, which every x86-64 CPU has, finds the end of runs of plain string characters 16 bytes at a time
and writes the digits of floating-point numbers. The UTF-8 check of non-ASCII text needs SSSE3, which is not part
of x86-64: it is compiled for SSSE3 with a function attribute and used when the CPU has SSSE3 (every x86-64 CPU
since about 2011), else the portable check. Code compiled for SSSE3 anyway (e.g., with `-mssse3` or `-march=native`)
skips this check of the CPU. 32-bit x86 uses SSE2 for strings when the compiler targets it.
- **AArch64:** NEON, which every 64-bit Arm CPU has, checks UTF-8 text and long runs of plain string characters 16
bytes at a time, and writes the digits of floating-point numbers. With MSVC, only the floating-point digits use
NEON.
- **Other targets:** portable code only.
Strings are only scanned this way for contiguous input and for the characters a `#!cpp std::istream` holds in its
stream buffer (see [`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other inputs are read one
character at a time.
!!! note "Interaction with `JSON_USE_SIMDUTF`"
With [`JSON_USE_SIMDUTF`](json_use_simdutf.md), runs of string characters are first validated by simdutf; the code
described here is used for what simdutf does not accept, so that errors are reported at the same position.
`JSON_NO_SIMD` does not affect simdutf.
!!! warning "Define consistently"
The macro selects between two definitions of the library's internal scanning and formatting functions. It must
therefore be defined identically for **every** translation unit that includes the library; mixing translation units
that define it with ones that do not is an ODR violation. Prefer setting it as a compile definition on the target
rather than with `#!cpp #define` in individual source files.
## Examples
??? example "Example: switching off vector instructions"
The program parses and serializes the same values with and without the macro:
```cpp
#define JSON_NO_SIMD
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
const json j = json::parse(R"({"name": "日本語 text", "pi": 3.141592653589793})");
std::cout << j.dump() << '\n'; // {"name":"日本語 text","pi":3.141592653589793}
}
```
## See also
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
- [**dump**](../basic_json/dump.md) - serialization
- [**JSON_USE_SIMDUTF**](json_use_simdutf.md) - use the simdutf library to accelerate UTF-8 validation
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
## Version history
- Added in version 3.13.0.
@@ -72,6 +72,12 @@ The default value is `0` (disabled — existing behavior is preserved).
distinct symbol names. Translation units compiled with and without it can therefore be linked into the same program
without One Definition Rule (ODR) violations, but they cannot exchange instances of library types.
!!! note "Bulk scanning"
The parser scans the characters in the stream buffer in bulk (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)), but it never reads ahead. The stream is left
at the same position with and without bulk scanning, in both modes of this macro.
!!! tip "Workaround without the macro"
Separate the values in the stream with whitespace. The character consumed after a number is then the separator,
@@ -6,14 +6,16 @@
When defined, the parser validates the UTF-8 content of JSON strings that come from a **contiguous byte input**
(`std::string`, `std::vector<char>`/`<std::uint8_t>`, string literals, `const char*` ranges, …) using the
[simdutf](https://github.com/simdutf/simdutf) library instead of the built-in scalar validator. On text with many
non-ASCII characters (e.g. CJK or emoji) this can validate several times faster.
[simdutf](https://github.com/simdutf/simdutf) library instead of the built-in validator (which uses SSSE3 or NEON where
available, see [`JSON_NO_SIMD`](json_no_simd.md)). On text with many non-ASCII characters (e.g. CJK or emoji) this can
validate faster, depending on the CPU.
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
message (simdutf is only used to fast-path *valid* runs; anything it flags falls back to the scalar path so the exact
diagnostic is preserved). Streaming inputs (files, `std::istream`, wide strings, user-defined adapters) always use the
scalar path.
diagnostic is preserved). A `std::istream` uses simdutf for the characters its stream buffer holds (see
[`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other streaming inputs (`FILE*`, wide strings,
user-defined adapters) always use the scalar path.
When `JSON_USE_SIMDUTF` is defined you must make the `simdutf.h` header available on the include path and link the
simdutf library. When it is not defined, no simdutf header is included and there is no dependency.
+4
View File
@@ -121,6 +121,7 @@ being read.
- [`JSON_STRICT_NUL_HANDLING`](macros/json_strict_nul_handling.md) - opt in to rejecting a NUL byte in the input
instead of treating it as end of input
- [`JSON_PRECISE_STREAM_POSITION`](macros/json_precise_stream_position.md) - opt in to leaving the stream positioned right after a number
- [`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) - read the stream one character at a time
## Version history
@@ -133,3 +134,6 @@ being read.
crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.0.
- Changed to the strong exception safety guarantee in version 3.13.0: `j` is no longer left with a partially parsed
value if parsing throws.
- Scans the characters in the stream buffer of `i` in bulk in version 3.13.0; the position of `i` after the call is
unchanged ([`JSON_DISABLE_STREAM_BULK_SCAN`](macros/json_disable_stream_bulk_scan.md) restores reading one character
at a time).
+23
View File
@@ -133,6 +133,14 @@ When defined, default parse and serialize functions for enums are excluded and h
See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md).
## `JSON_DISABLE_STREAM_BULK_SCAN`
When defined to `1`, parsing from a `#!cpp std::istream` reads one character at a time instead of scanning the
characters that the stream buffer holds in bulk. Results are the same either way; the macro is an escape hatch for
stream buffers that do not follow the usual `std::streambuf` contract. The default value is `0`.
See [full documentation of `JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md).
## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`
When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON
@@ -141,6 +149,14 @@ is planned to become the default in version 4.0.0.
See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md).
## `JSON_DISABLE_UNLOCKED_FILE_IO`
When defined to `1`, parsing from a `#!cpp FILE*` reads every character with `std::fgetc`, which locks the file each
time, instead of locking the file once and reading it with the unlocked stdio functions. Results are the same either
way. The default value is `0`.
See [full documentation of `JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md).
## `JSON_NO_AUTOMATIC_UDLS`
When defined, `<nlohmann/json.hpp>` does not include `<nlohmann/json_literals.hpp>` with the user-defined string literals
@@ -158,6 +174,13 @@ security reasons (e.g., Intel Software Guard Extensions (SGX)).
See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md).
## `JSON_NO_SIMD`
When defined, the library uses only portable code instead of vector instructions (SSE2, SSSE3, NEON) for scanning
strings and writing floating-point numbers. Results are the same either way. It is not defined by default.
See [full documentation of `JSON_NO_SIMD`](../api/macros/json_no_simd.md).
## `JSON_NO_THREAD_LOCAL`
When defined, the library does not use `#!cpp thread_local` storage. Copying a value and comparing two values then
@@ -55,6 +55,34 @@ whitespace, because `operator>>` consumes the character that terminates a number
[`JSON_PRECISE_STREAM_POSITION`](../../api/macros/json_precise_stream_position.md) is defined to `1` — see the
[`operator>>` notes](../../api/operator_gtgt.md#notes) for details and examples.
## Input sources and performance
All inputs produce the same values and the same exceptions, but they are read in different ways, which makes some of
them faster than others:
| input | how it is read |
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time |
| `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up |
| `#!cpp FILE*` | one character at a time, with the file locked once for the whole parse |
| other iterator ranges (e.g., `#!cpp std::list<char>`, `#!cpp std::istreambuf_iterator<char>`), wide strings | one character at a time |
Parsing a file from a `#!cpp std::ifstream` is therefore only about 10 to 20 % slower than reading the whole file into a
`#!cpp std::string` first and parsing that, and it needs no memory for a copy of the file. The stream is never read
ahead: after parsing, it is positioned exactly as if every character had been read one at a time, so
[`operator>>`](../../api/operator_gtgt.md) can still read several values from one stream (see also
[JSON Lines](json_lines.md)).
Two macros switch these optimizations off without changing any result:
[`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not
follow the usual `std::streambuf` contract, and
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another
thread must use the same `#!cpp FILE` object while it is parsed.
Where the target has them, strings are scanned with vector instructions: SSE2 on x86-64 (and SSSE3 for UTF-8 text, when
the CPU has it) and NEON on AArch64. [`JSON_NO_SIMD`](../../api/macros/json_no_simd.md) selects portable code instead,
again without changing any result.
## SAX vs. DOM parsing
The library offers two parsing models:
+8 -8
View File
@@ -120,7 +120,7 @@ requirements are collected in [Template Parameter Requirements](template_paramet
### Default type
With the default values for *ObjectType* (`std::map`), *StringType* (`std::string`), and *AllocatorType* (`std::allocator`), the default value for `object_t` is:
With the default values for *ObjectType* (`std::map`), *StringType* (`std::string`), and *AllocatorType* (`std::allocator`), the default type for `object_t` is:
```cpp
std::map<
@@ -177,7 +177,7 @@ json j = m;
### Default type
With the default values for *ArrayType* (`std::vector`) and *AllocatorType* (`std::allocator`), the default value for `array_t` is:
With the default values for *ArrayType* (`std::vector`) and *AllocatorType* (`std::allocator`), the default type for `array_t` is:
```cpp
std::vector<
@@ -209,7 +209,7 @@ Unicode values are split by the JSON class into byte-sized characters during des
### Default type
With the default values for *StringType* (`std::string`), the default value for `string_t` is `#!cpp std::string`.
With the default values for *StringType* (`std::string`), the default type for `string_t` is `#!cpp std::string`.
### Encoding
@@ -234,7 +234,7 @@ String values are stored as pointers in a `basic_json` type. That is, for any ac
### Default type
With the default values for *BooleanType* (`#!cpp bool`), the default value for `boolean_t` is `#!cpp bool`.
With the default values for *BooleanType* (`#!cpp bool`), the default type for `boolean_t` is `#!cpp bool`.
### Storage
@@ -252,9 +252,9 @@ This description includes both integer and floating-point numbers. However, C++
### Default types
With the default values for *NumberIntegerType* (`std::int64_t`), the default value for `number_integer_t` is `std::int64_t`.
With the default values for *NumberUnsignedType* (`std::uint64_t`), the default value for `number_unsigned_t` is `std::uint64_t`.
With the default values for *NumberFloatType* (`#!cpp double`), the default value for `number_float_t` is `#!cpp double`.
With the default values for *NumberIntegerType* (`std::int64_t`), the default type for `number_integer_t` is `std::int64_t`.
With the default values for *NumberUnsignedType* (`std::uint64_t`), the default type for `number_unsigned_t` is `std::uint64_t`.
With the default values for *NumberFloatType* (`#!cpp double`), the default type for `number_float_t` is `#!cpp double`.
### Default behavior
@@ -281,7 +281,7 @@ As this range is a subrange of the exactly supported range [`INT64_MIN`, `INT64_
> This specification allows implementations to set limits on the range and precision of numbers accepted. Since software that implements IEEE 754-2008 binary64 (double precision) numbers is generally available and widely used, good interoperability can be achieved by implementations that expect no more precision or range than these provide, in the sense that implementations will approximate JSON numbers within the expected precision.
This implementation does exactly follow this approach, as it uses double precision floating-point numbers. Note that numbers smaller than `#!c -1.79769313486232e+308` or greater than `#!c 1.79769313486232e+308` are rejected during parsing with [`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406). Non-finite values (NaN and infinity) can only be stored by assigning them directly and are serialized to `#!json null`.
This implementation does exactly follow this approach, as it uses double precision floating-point numbers by default (see *NumberFloatType* above). Note that with the default `#!cpp double`, numbers smaller than `#!c -1.79769313486232e+308` or greater than `#!c 1.79769313486232e+308` are rejected during parsing with [`out_of_range.406`](../../home/exceptions.md#jsonexceptionout_of_range406). Non-finite values (NaN and infinity) can only be stored by assigning them directly and are serialized to `#!json null`.
### Storage
@@ -345,8 +345,11 @@ The number types can be changed with template parameters.
in case of overflow.
- The type for unsigned integers must be convertible from `#!c unsigned long long`. The type for floating-point
numbers is used in case of overflow.
- The types for signed and unsigned integers must be distinct, see
[#2573](https://github.com/nlohmann/json/issues/2573).
- The type for unsigned integers can also be the same signed type as the type for signed integers (e.g.,
`#!c std::int64_t` for both). Then every integer is stored as a signed integer, and
[`is_number_unsigned()`](../../api/basic_json/is_number_unsigned.md) is always `#!c false`, see
[Template Parameter Requirements](template_parameters.md#same-type-for-both).
- The type for floating-point numbers must be distinct from the integer types.
- Only `#!c double`, `#!c float`, and `#!c long double` are supported for floating-point numbers.
!!! example
@@ -40,19 +40,19 @@ Requirements are split into two groups:
## Overview
| Template parameter | Default | Notable substitutes |
|-------------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|
| [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps |
| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` |
| [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` |
| [`BooleanType`](#booleantype) | `bool` | none worth using |
| [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type |
| [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) | `std::uint64_t` | any unsigned integer type at least as wide as `NumberIntegerType` |
| [`NumberFloatType`](#numberfloattype) | `double` | `float` (`long double`: no binary formats) |
| [`AllocatorType`](#allocatortype) | `std::allocator` | stateless allocators |
| [`JSONSerializer`](#jsonserializer) | `adl_serializer` | serializers with the same interface |
| [`BinaryType`](#binarytype) | `#!cpp std::vector<std::uint8_t>` | `#!cpp std::vector<char>` |
| [`CustomBaseClass`](#custombaseclass) | `void` | any default-constructible class |
| Template parameter | Default | Notable substitutes |
|-------------------------------------------------------------------|-----------------------------------|--------------------------------------------------------------------------------------------------|
| [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps |
| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` |
| [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` |
| [`BooleanType`](#booleantype) | `bool` | none worth using |
| [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type |
| [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) | `std::uint64_t` | any unsigned integer type at least as wide as `NumberIntegerType`, or `NumberIntegerType` itself |
| [`NumberFloatType`](#numberfloattype) | `double` | `float` (`long double`: no binary formats) |
| [`AllocatorType`](#allocatortype) | `std::allocator` | stateless allocators |
| [`JSONSerializer`](#jsonserializer) | `adl_serializer` | serializers with the same interface |
| [`BinaryType`](#binarytype) | `#!cpp std::vector<std::uint8_t>` | `#!cpp std::vector<char>` |
| [`CustomBaseClass`](#custombaseclass) | `void` | any default-constructible class |
!!! warning "Third-party containers and incomplete types"
@@ -504,13 +504,14 @@ Both types are stored **directly** inside `basic_json`'s union.
### Always required
- `#!cpp std::is_integral` must be satisfied: `NumberIntegerType` must be a **signed** integer type,
`NumberUnsignedType` an **unsigned** integer type. Class types are not supported -- among others, the constructors
taking integer values are constrained on `#!cpp std::is_integral`.
`NumberUnsignedType` an **unsigned** integer type or the same type as `NumberIntegerType` (see
[below](#same-type-for-both)). Class types are not supported -- among others, the constructors taking integer values
are constrained on `#!cpp std::is_integral`.
- Trivially default-constructible, trivially copyable, and trivially destructible (union member).
- `#!cpp std::numeric_limits` must be specialized for both types.
- `NumberUnsignedType` must be able to represent the absolute value of every `NumberIntegerType` value; serialization
of negative numbers converts the value to `NumberUnsignedType`. A `#!cpp static_assert` requires it to be at least as
wide as `NumberIntegerType`, which is what that amounts to for the standard integer types.
- An unsigned `NumberUnsignedType` must be able to represent the absolute value of every `NumberIntegerType` value;
serialization of negative numbers converts the value to `NumberUnsignedType`. A `#!cpp static_assert` requires it to
be at least as wide as `NumberIntegerType`, which is what that amounts to for the standard integer types.
- Both types must fit into the internal 64-character number buffer used by
[`dump`](../../api/basic_json/dump.md), which is the case for all standard integer types.
- [`std::hash<basic_json>`](../../api/basic_json/std_hash.md) additionally requires `#!cpp std::hash` specializations.
@@ -522,6 +523,23 @@ is stored as [`number_float_t`](../../api/basic_json/number_float_t.md) instead.
therefore silently changes parse results rather than raising an error. See
[Number Handling](number_handling.md) for details.
#### Same type for both
`NumberUnsignedType` may be the same type as `NumberIntegerType`, e.g.
`#!cpp nlohmann::json::with_integers_t<std::int64_t, std::int64_t>` (see
[`with_integers_t`](../../api/basic_json/with_t.md)), for code that only wants to handle one integer type. Such a `basic_json` stores every integer as
[`number_integer_t`](../../api/basic_json/number_integer_t.md):
- [`is_number_unsigned()`](../../api/basic_json/is_number_unsigned.md) is always `#!cpp false`, and
[`type()`](../../api/basic_json/type.md) never returns `value_t::number_unsigned`. Constructing a value from
`value_t::number_unsigned` gives a `value_t::number_integer` value.
- Values of unsigned C++ types (e.g., `#!cpp 5u`) are converted to `NumberIntegerType`, so values above its maximum
wrap around, as values outside the range of a narrower `NumberIntegerType` already do.
- The parsers store a non-negative integer above the maximum of `NumberIntegerType` (e.g., `18446744073709551615` for
`#!cpp std::int64_t`) as [`number_float_t`](../../api/basic_json/number_float_t.md), like any other integer that does
not fit.
- Converting from a `basic_json` with distinct types turns its unsigned values into `number_integer_t` values.
### Compatible types
| Type pair | Support |
@@ -529,8 +547,9 @@ therefore silently changes parse results rather than raising an error. See
| `#!cpp std::int64_t` / `#!cpp std::uint64_t` (default) | full |
| `#!cpp std::int32_t` / `#!cpp std::uint32_t`, `#!cpp long long` / `#!cpp unsigned long long` | full; narrower types change which literals the parser can represent |
| any other pair of standard signed/unsigned integer types | full |
| the same signed integer type for both, e.g. `#!cpp std::int64_t` / `#!cpp std::int64_t` | full; every integer is stored as `number_integer_t`, see [above](#same-type-for-both) |
| class types, enumerations | not usable; `#!cpp std::is_integral` must hold |
| `#!cpp bool`, or a type already used for another member of the union | not usable; `#!cpp std::is_integral<bool>` is in fact `#!cpp true`, but the `get_impl_ptr` overloads for `boolean_t`, `number_integer_t`, `number_unsigned_t` and `number_float_t` would collide |
| `#!cpp bool`, `number_float_t`, or the type of another member of the union | not usable; `#!cpp std::is_integral<bool>` is in fact `#!cpp true`, but the `get_impl_ptr` overloads for `boolean_t`, `number_integer_t`, `number_unsigned_t` and `number_float_t` would collide |
## `NumberFloatType`
+1 -1
View File
@@ -59,7 +59,7 @@ below are relative to `include/nlohmann`.
|-----------|----------|
| Value type enumeration | [`detail/value_t.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/value_t.hpp) |
| Input adapters | [`detail/input/input_adapters.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/input_adapters.hpp) |
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp) |
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp), [`detail/input/simd_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/simd_scan.hpp) |
| Parser | [`detail/input/parser.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/parser.hpp) |
| SAX interface and DOM builders | [`detail/input/json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp) |
| Binary format readers | [`detail/input/binary_reader.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/binary_reader.hpp) |
+9
View File
@@ -9,6 +9,15 @@ published on the [GitHub releases page](https://github.com/nlohmann/json/release
This documentation is built from the `develop` branch and may describe changes that are not part of a release
yet. Their version numbers are followed by an <span class="unreleased-version">unreleased</span> badge.
## Unreleased
- Converting an rvalue container to JSON (`json j = std::move(vec);`, `json j = std::map<std::string, std::string>{...};`)
now **moves** the elements instead of copying them (e.g., `std::vector<std::string>` with 100,000 strings: 4.7 ms to
2.3 ms). Two consequences: the source container is left in a valid but unspecified (moved-from) state, and if an
exception is thrown during the conversion, the source may be partially moved-from, so the strong exception guarantee
no longer holds for the source of an rvalue conversion (the JSON value itself is unaffected). Lvalues and `const`
objects are still copied. See [`basic_json`](../api/basic_json/basic_json.md).
## v3.12.0 (2025-04-11)
Fixes bugs found in 3.11.3 and adds several features. All changes are backward-compatible.
+3
View File
@@ -297,7 +297,9 @@ nav:
- 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md
- 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.md
- 'JSON_DISABLE_ENUM_SERIALIZATION': api/macros/json_disable_enum_serialization.md
- 'JSON_DISABLE_STREAM_BULK_SCAN': api/macros/json_disable_stream_bulk_scan.md
- 'JSON_DISABLE_TUPLE_REFERENCE_CONVERSION': api/macros/json_disable_tuple_reference_conversion.md
- 'JSON_DISABLE_UNLOCKED_FILE_IO': api/macros/json_disable_unlocked_file_io.md
- 'JSON_HAS_CPP_11, JSON_HAS_CPP_14, JSON_HAS_CPP_17, JSON_HAS_CPP_20, JSON_HAS_CPP_23, JSON_HAS_CPP_26': api/macros/json_has_cpp_11.md
- 'JSON_HAS_EXPERIMENTAL_FILESYSTEM, JSON_HAS_FILESYSTEM': api/macros/json_has_filesystem.md
- 'JSON_HAS_RANGE_VIEW_CONVERSION': api/macros/json_has_range_view_conversion.md
@@ -308,6 +310,7 @@ nav:
- 'JSON_NO_AUTOMATIC_UDLS': api/macros/json_no_automatic_udls.md
- 'JSON_NOEXCEPTION': api/macros/json_noexception.md
- 'JSON_NO_IO': api/macros/json_no_io.md
- 'JSON_NO_SIMD': api/macros/json_no_simd.md
- 'JSON_NO_THREAD_LOCAL': api/macros/json_no_thread_local.md
- 'JSON_PRECISE_STREAM_POSITION': api/macros/json_precise_stream_position.md
- 'JSON_SKIP_LIBRARY_VERSION_CHECK': api/macros/json_skip_library_version_check.md
@@ -80,7 +80,11 @@ void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val)
{
case value_t::number_unsigned:
{
val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_unsigned_t*>());
// never null: a basic_json whose number_unsigned_t is number_integer_t
// stores no number_unsigned values (#2573)
const auto* ptr = j.template get_ptr<const typename BasicJsonType::number_unsigned_t*>();
JSON_ASSERT(ptr != nullptr);
val = static_cast<ArithmeticType>(*ptr);
break;
}
case value_t::number_integer:
@@ -150,7 +154,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_flo
}
template<typename BasicJsonType>
inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val)
inline void from_json(const BasicJsonType& j, number_unsigned_overload_t<BasicJsonType>& val)
{
get_arithmetic_value(j, val);
}
@@ -266,6 +270,22 @@ template<typename ConstructibleArrayType>
void from_json_array_reserve(ConstructibleArrayType& /*arr*/, std::size_t /*size*/, priority_tag<0> /*unused*/)
{}
// append through push_back() where available; containers without it (such as
// std::set) fall back to insert(end(), ...), which is what std::inserter did
template<typename ConstructibleArrayType, typename ValueType>
auto from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<1> /*unused*/)
-> decltype(arr.push_back(std::forward<ValueType>(value)), void())
{
arr.push_back(std::forward<ValueType>(value));
}
template<typename ConstructibleArrayType, typename ValueType>
void from_json_array_append(ConstructibleArrayType& arr, ValueType&& value, priority_tag<0> /*unused*/)
{
using std::end;
arr.insert(end(arr), std::forward<ValueType>(value));
}
template<typename BasicJsonType, typename ConstructibleArrayType,
enable_if_t<
std::is_assignable<ConstructibleArrayType&, ConstructibleArrayType>::value,
@@ -275,17 +295,15 @@ auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, p
j.template get<typename ConstructibleArrayType::value_type>(),
void())
{
using std::end;
ConstructibleArrayType ret;
from_json_array_reserve(ret, j.size(), priority_tag<1> {});
std::transform(j.begin(), j.end(),
std::inserter(ret, end(ret)), [](const BasicJsonType & i)
const auto* inner_array = j.template get_ptr<const typename BasicJsonType::array_t*>();
from_json_array_reserve(ret, inner_array->size(), priority_tag<1> {});
for (const auto& elem : *inner_array)
{
// get<BasicJsonType>() returns *this, this won't call a from_json
// method when value_type is BasicJsonType
return i.template get<typename ConstructibleArrayType::value_type>();
});
from_json_array_append(ret, elem.template get<typename ConstructibleArrayType::value_type>(), priority_tag<1> {});
}
arr = std::move(ret);
}
@@ -371,6 +389,23 @@ template<typename ConstructibleObjectType>
inline void from_json_object_reserve(ConstructibleObjectType& /*obj*/, std::size_t /*size*/, priority_tag<0> /*unused*/)
{}
// the keys of the source are sorted for the usual object types, so hinting the
// end makes every insertion constant-time (for containers that order their keys
// the same way); containers without emplace_hint() (such as ordered_map) fall
// back to emplace()
template<typename ConstructibleObjectType, typename Key, typename Value>
auto from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<1> /*unused*/)
-> decltype(obj.emplace_hint(obj.end(), std::forward<Key>(key), std::forward<Value>(value)), void())
{
obj.emplace_hint(obj.end(), std::forward<Key>(key), std::forward<Value>(value));
}
template<typename ConstructibleObjectType, typename Key, typename Value>
void from_json_object_emplace(ConstructibleObjectType& obj, Key&& key, Value&& value, priority_tag<0> /*unused*/)
{
obj.emplace(std::forward<Key>(key), std::forward<Value>(value));
}
template<typename BasicJsonType, typename ConstructibleObjectType,
enable_if_t<is_constructible_object_type<BasicJsonType, ConstructibleObjectType>::value, int> = 0>
inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj)
@@ -385,7 +420,7 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj)
from_json_object_reserve(ret, inner_object->size(), priority_tag<1> {});
for (const auto& p : *inner_object)
{
ret.emplace(p.first, p.second.template get<typename ConstructibleObjectType::mapped_type>());
from_json_object_emplace(ret, p.first, p.second.template get<typename ConstructibleObjectType::mapped_type>(), priority_tag<1> {});
}
obj = std::move(ret);
}
@@ -406,7 +441,11 @@ inline void from_json(const BasicJsonType& j, ArithmeticType& val)
{
case value_t::number_unsigned:
{
val = static_cast<ArithmeticType>(*j.template get_ptr<const typename BasicJsonType::number_unsigned_t*>());
// never null: a basic_json whose number_unsigned_t is number_integer_t
// stores no number_unsigned values (#2573)
const auto* ptr = j.template get_ptr<const typename BasicJsonType::number_unsigned_t*>();
JSON_ASSERT(ptr != nullptr);
val = static_cast<ArithmeticType>(*ptr);
break;
}
case value_t::number_integer:
@@ -23,12 +23,12 @@
#endif
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
// digits of a double at once
#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC))
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
#include <emmintrin.h>
#define JSON_DTOA_SSE2 1
#define JSON_DTOA_NEON 0
#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN)
#elif !defined(JSON_NO_SIMD) && (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN)
#include <arm_neon.h>
#define JSON_DTOA_SSE2 0
#define JSON_DTOA_NEON 1
@@ -167,6 +167,17 @@ struct external_constructor<value_t::array>
j.replace_value(value_t::array, value);
}
/// an rvalue container: move its elements
template<typename BasicJsonType, typename CompatibleArrayType>
static void construct_moving(BasicJsonType& j, CompatibleArrayType& arr)
{
using std::begin;
using std::end;
const typename BasicJsonType::json_value value(j.template create<typename BasicJsonType::array_t>(std::make_move_iterator(begin(arr)), std::make_move_iterator(end(arr))));
j.replace_value(value_t::array, value);
}
template<typename BasicJsonType>
static void construct(BasicJsonType& j, const std::vector<bool>& arr)
{
@@ -217,6 +228,17 @@ struct external_constructor<value_t::object>
j.replace_value(value_t::object, value);
}
/// an rvalue container: move its values
template<typename BasicJsonType, typename CompatibleObjectType>
static void construct_moving(BasicJsonType& j, CompatibleObjectType& obj)
{
using std::begin;
using std::end;
const typename BasicJsonType::json_value value(j.template create<typename BasicJsonType::object_t>(std::make_move_iterator(begin(obj)), std::make_move_iterator(end(obj))));
j.replace_value(value_t::object, value);
}
template < typename BasicJsonType, typename CompatibleObjectType,
enable_if_t < !std::is_same<CompatibleObjectType, typename BasicJsonType::object_t>::value, int > = 0 >
static void construct(BasicJsonType& j, const CompatibleObjectType& obj)
@@ -303,14 +325,14 @@ inline void to_json(BasicJsonType& j, FloatType val) noexcept
}
template<typename BasicJsonType, typename CompatibleNumberUnsignedType,
enable_if_t<is_compatible_integer_type<typename BasicJsonType::number_unsigned_t, CompatibleNumberUnsignedType>::value, int> = 0>
enable_if_t<is_compatible_integer_type<number_unsigned_overload_t<BasicJsonType>, CompatibleNumberUnsignedType>::value, int> = 0>
inline void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept
{
external_constructor<value_t::number_unsigned>::construct(j, static_cast<typename BasicJsonType::number_unsigned_t>(val));
}
template<typename BasicJsonType, typename CompatibleNumberIntegerType,
enable_if_t<is_compatible_integer_type<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType>::value, int> = 0>
enable_if_t<is_compatible_number_integer_type<BasicJsonType, CompatibleNumberIntegerType>::value, int> = 0>
inline void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept
{
external_constructor<value_t::number_integer>::construct(j, static_cast<typename BasicJsonType::number_integer_t>(val));
@@ -354,6 +376,66 @@ inline void to_json(BasicJsonType& j, const CompatibleArrayType& arr)
external_constructor<value_t::array>::construct(j, arr);
}
// types that have a dedicated to_json overload and must not be treated as generic containers
template<typename T>
struct has_dedicated_to_json : std::false_type {};
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
template<>
struct has_dedicated_to_json<std_fs::path> : std::true_type {};
#endif
#if defined(__cpp_lib_char8_t)
template<typename Tr, typename Allocator>
struct has_dedicated_to_json<std::basic_string<char8_t, Tr, Allocator>> : std::true_type {};
#endif
#if JSON_HAS_RANGE_VIEW_CONVERSION
template<typename T>
struct is_range_view_type : is_compatible_range_view<T> {};
#else
template<typename T>
struct is_range_view_type : std::false_type {};
#endif
#if JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS
template<typename T>
struct is_enum_keyed_map_type : is_enum_keyed_map<T> {};
#else
template<typename T>
struct is_enum_keyed_map_type : std::false_type {};
#endif
// rvalue (non-const) container: the generic to_json takes its elements by move
template<typename T>
using is_movable_container_rvalue = negation<disjunction<
std::is_lvalue_reference<T>,
std::is_const<T>,
has_dedicated_to_json<T>,
is_basic_json<T>,
is_range_view_type<T>,
is_enum_keyed_map_type<T>,
std::is_same<T, std::vector<bool>>>>;
// rvalue array-like containers
template<typename BasicJsonType, typename T>
using is_rvalue_array_to_json = conjunction<is_movable_container_rvalue<T>,
is_compatible_array_type<BasicJsonType, T>,
negation<disjunction<
std::is_same<T, typename BasicJsonType::array_t>,
std::is_same<T, typename BasicJsonType::binary_t>,
is_compatible_object_type<BasicJsonType, T>,
is_compatible_string_type<BasicJsonType, T>,
is_compatible_binary_type<BasicJsonType, T>>>>;
// rvalue containers: their elements are moved instead of copied
template<typename BasicJsonType, typename CompatibleArrayType,
enable_if_t<is_rvalue_array_to_json<BasicJsonType, CompatibleArrayType>::value, int> = 0>
inline void to_json(BasicJsonType& j, CompatibleArrayType && arr)
{
external_constructor<value_t::array>::construct_moving(j, arr);
}
#if JSON_HAS_RANGE_VIEW_CONVERSION
template < typename BasicJsonType, typename T,
enable_if_t < is_compatible_range_view<std::remove_cvref_t<T>>::value
@@ -427,6 +509,19 @@ inline void to_json(BasicJsonType& j, const EnumKeyedMap& map)
}
#endif
// rvalue object-like containers
template<typename BasicJsonType, typename T>
using is_rvalue_object_to_json = conjunction<is_movable_container_rvalue<T>,
negation<std::is_same<T, typename BasicJsonType::object_t>>,
is_compatible_object_type<BasicJsonType, T>>;
template<typename BasicJsonType, typename CompatibleObjectType,
enable_if_t<is_rvalue_object_to_json<BasicJsonType, CompatibleObjectType>::value, int> = 0>
inline void to_json(BasicJsonType& j, CompatibleObjectType && obj)
{
external_constructor<value_t::object>::construct_moving(j, obj);
}
template<typename BasicJsonType>
inline void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj)
{
+4 -1
View File
@@ -184,7 +184,10 @@ JSON_HEDLEY_ALWAYS_INLINE shortest_decimal to_shortest(std::uint64_t bits) noexc
const uint128_parts p10 = pow10(-dec_exp - 1);
const uint128_parts p = umul192_hi128(p10.high, p10.low, bin_sig << static_cast<unsigned>(shift));
std::uint64_t integral = p.high >> static_cast<unsigned>(extra_shift);
const std::uint64_t fractional = (p.high << static_cast<unsigned>(64 - extra_shift)) | (p.low >> static_cast<unsigned>(extra_shift));
// (no casts in the shifts: MSVC 2015 warns C4554 on them despite the parentheses)
constexpr unsigned fractional_low_shift = static_cast<unsigned>(extra_shift);
constexpr unsigned fractional_high_shift = 64u - fractional_low_shift;
const std::uint64_t fractional = (p.high << fractional_high_shift) | (p.low >> fractional_low_shift);
std::uint64_t digit = 0;
bool round_up = false;
bool round_down = false;
+73 -2
View File
@@ -428,7 +428,7 @@ void templated_json_throw(ExceptionType exception)
@throw type_error.302 always
*/
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN inline void throw_type_must_be(const char* expected, const BasicJsonType& j)
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const char* expected, const BasicJsonType& j)
{
static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(j);
@@ -443,7 +443,7 @@ JSON_HEDLEY_NO_RETURN inline void throw_type_must_be(const char* expected, const
@throw type_error always
*/
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN inline void throw_cannot_use_with(const exception_id id_, const char* operation, const BasicJsonType& j)
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_use_with(const exception_id id_, const char* operation, const BasicJsonType& j)
{
static_cast<void>(id_); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(operation);
@@ -451,6 +451,77 @@ JSON_HEDLEY_NO_RETURN inline void throw_cannot_use_with(const exception_id id_,
JSON_THROW(type_error::create(id_, concat("cannot use ", operation, " with ", j.type_name()), &j));
}
/*!
@brief throws ExceptionType with a fixed message; kept out of line so that callers do not carry the exception construction
*/
template<typename ExceptionType, typename BasicJsonContext>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_literal(const exception_id id_, const char* what_arg, BasicJsonContext context)
{
static_cast<void>(id_); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(what_arg);
static_cast<void>(context);
JSON_THROW(ExceptionType::create(id_, what_arg, context));
}
// JSON Patch errors; constructed out of line so that patch_inplace() does not carry them
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_add_parent(const BasicJsonType& parent)
{
static_cast<void>(parent);
JSON_THROW(out_of_range::create(exception_id::patch_add_parent_not_container, concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
}
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_remove_parent(const BasicJsonType& parent)
{
static_cast<void>(parent);
JSON_THROW(out_of_range::create(exception_id::patch_remove_parent_not_container, concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
}
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_not_an_array(const BasicJsonType& j)
{
static_cast<void>(j);
JSON_THROW(parse_error::create(exception_id::patch_not_an_array, 0, "JSON patch must be an array of objects", &j));
}
template<typename StringType, typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_missing_member(const StringType& op, const StringType& member, const bool string_type, const BasicJsonType& val)
{
static_cast<void>(op);
static_cast<void>(member);
static_cast<void>(string_type);
static_cast<void>(val);
// context-sensitive error message
const std::string error_msg = (op == "op") ? std::string("operation") : concat("operation '", op, '\'');
JSON_THROW(parse_error::create(exception_id::patch_invalid_operation, 0, concat(error_msg, string_type ? " must have string member '" : " must have member '", member, "'"), &val));
}
template<typename StringType, typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_move_into_child(const StringType& from_path, const StringType& path, const BasicJsonType& result)
{
static_cast<void>(from_path);
static_cast<void>(path);
static_cast<void>(result);
JSON_THROW(out_of_range::create(exception_id::patch_move_into_child, concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result));
}
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_test_failed(const BasicJsonType& val)
{
static_cast<void>(val);
JSON_THROW(other_error::create(exception_id::patch_test_failed, concat("unsuccessful: ", val.dump()), &val));
}
template<typename StringType, typename BasicJsonType>
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_patch_invalid_operation(const StringType& op, const BasicJsonType& val)
{
static_cast<void>(op);
static_cast<void>(val);
JSON_THROW(parse_error::create(exception_id::patch_invalid_operation, 0, concat("operation value '", op, "' is invalid"), &val));
}
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END
+89 -33
View File
@@ -195,7 +195,7 @@ class binary_reader
if (JSON_HEDLEY_UNLIKELY(current != char_traits<char_type>::eof()))
{
return last_byte_error(exception_id::unexpected_end_of_input, concat("expected end of input; last byte: 0x", get_token_string()), "value");
return last_byte_error(exception_id::unexpected_end_of_input, "expected end of input", "value", "");
}
}
@@ -317,7 +317,7 @@ class binary_reader
{
if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast<std::size_t>(document_size) != chars_read - document_start))
{
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(chars_read - document_start), ")"), "document");
return bson_document_size_error(document_size, chars_read - document_start);
}
return true;
}
@@ -509,7 +509,7 @@ class binary_reader
{
if (JSON_HEDLEY_UNLIKELY(len < 1))
{
return last_byte_error(exception_id::unexpected_byte, concat("string length must be at least 1, is ", std::to_string(len)), "string");
return bson_length_error("string length must be at least 1, is ", len, "string");
}
if (JSON_HEDLEY_UNLIKELY(!get_string(len - static_cast<NumberType>(1), result)))
@@ -539,7 +539,7 @@ class binary_reader
{
if (JSON_HEDLEY_UNLIKELY(len < 0))
{
return last_byte_error(exception_id::unexpected_byte, concat("byte array length cannot be negative, is ", std::to_string(len)), "binary");
return bson_length_error("byte array length cannot be negative, is ", len, "binary");
}
// All BSON binary values have a subtype
@@ -629,9 +629,7 @@ class binary_reader
default: // anything else is not supported (yet)
{
const std::string cr_str = hex_byte(static_cast<std::uint8_t>(element_type));
return sax->parse_error(element_type_parse_position, cr_str,
parse_error::create(exception_id::bson_unsupported_type, element_type_parse_position, concat("Unsupported BSON record type 0x", cr_str), nullptr));
return bson_unsupported_type_error(element_type_parse_position, element_type);
}
}
}
@@ -1060,6 +1058,7 @@ class binary_reader
@param[in] context parsing context for the error message
@return whether the SAX consumer accepts the parse error
*/
JSON_HEDLEY_NEVER_INLINE
bool cbor_indefinite_string_error(const char* type_name, const char* context)
{
return last_byte_error(exception_id::invalid_string_or_size, concat("indefinite-length ", type_name, " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", get_token_string()), context);
@@ -1139,7 +1138,7 @@ class binary_reader
default:
{
return last_byte_error(exception_id::invalid_string_or_size, concat("expected length specification (0x60-0x7B)", inside_indefinite ? "" : " or indefinite string type (0x7F)", "; last byte: 0x", get_token_string()), "string");
return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x60-0x7B)", "string", inside_indefinite ? "" : " or indefinite string type (0x7F)");
}
}
}
@@ -1269,7 +1268,7 @@ class binary_reader
break;
}
return last_byte_error(exception_id::invalid_string_or_size, concat("only string keys are supported, but found ", found, "; last byte: 0x", get_token_string()), "object key");
return last_byte_error(exception_id::invalid_string_or_size, "only string keys are supported, but found ", "object key", found);
}
/*!
@@ -1350,7 +1349,7 @@ class binary_reader
default:
{
return last_byte_error(exception_id::invalid_string_or_size, concat("expected length specification (0x40-0x5B)", inside_indefinite ? "" : " or indefinite binary array type (0x5F)", "; last byte: 0x", get_token_string()), "binary");
return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x40-0x5B)", "binary", inside_indefinite ? "" : " or indefinite binary array type (0x5F)");
}
}
}
@@ -1496,8 +1495,7 @@ class binary_reader
{
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(len) || len == detail::unknown_size()))
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large,
exception_message(concat("excessive ", context, " size"), "size"), nullptr));
return container_size_error(context);
}
result = conditional_static_cast<std::size_t>(len);
return true;
@@ -2065,7 +2063,7 @@ class binary_reader
default:
{
return last_byte_error(exception_id::invalid_string_or_size, concat("expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x", get_token_string()), "string");
return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0xA0-0xBF, 0xD9-0xDB)", "string", "");
}
}
}
@@ -2157,7 +2155,7 @@ class binary_reader
break;
}
return last_byte_error(exception_id::invalid_string_or_size, concat("only string keys are supported, but found ", found, "; last byte: 0x", get_token_string()), "object key");
return last_byte_error(exception_id::invalid_string_or_size, "only string keys are supported, but found ", "object key", found);
}
/*!
@@ -2655,8 +2653,7 @@ class binary_reader
}
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(number)))
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large,
exception_message("integer value overflow", "size"), nullptr));
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = static_cast<std::size_t>(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char
return true;
@@ -2753,8 +2750,7 @@ class binary_reader
}
if (!value_in_range_of<std::size_t>(number))
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large,
exception_message("integer value overflow", "size"), nullptr));
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = detail::conditional_static_cast<std::size_t>(number);
return true;
@@ -2832,7 +2828,7 @@ class binary_reader
// or SIZE_MAX.
if (JSON_HEDLEY_UNLIKELY(result > (std::numeric_limits<std::size_t>::max)() / i))
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, exception_message("excessive ndarray size caused overflow", "size"), nullptr));
return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size");
}
result *= i;
// the pre-check above already rules out result becoming 0
@@ -2841,7 +2837,7 @@ class binary_reader
// unknown-size container (see get_ubjson_size_type())
if (result == npos)
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, exception_message("excessive ndarray size caused overflow", "size"), nullptr));
return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size");
}
if (JSON_HEDLEY_UNLIKELY(!emit_unsigned(i)))
{
@@ -2891,7 +2887,7 @@ class binary_reader
if (input_format == input_format_t::bjdata
&& JSON_HEDLEY_UNLIKELY(is_bjd_excluded_optimized_type(result.second)))
{
return last_byte_error(exception_id::unexpected_byte, concat("marker 0x", get_token_string(), " is not a permitted optimized array type"), "type");
return optimized_type_error();
}
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("type")))
@@ -2906,7 +2902,7 @@ class binary_reader
{
return false;
}
return last_byte_error(exception_id::unexpected_byte, concat("expected '#' after type information; last byte: 0x", get_token_string()), "size");
return last_byte_error(exception_id::unexpected_byte, "expected '#' after type information", "size", "");
}
const bool is_error = get_ubjson_size_value(result.first, is_ndarray, 0, result.second);
@@ -3057,7 +3053,7 @@ class binary_reader
}
if (JSON_HEDLEY_UNLIKELY(current > 127))
{
return last_byte_error(exception_id::invalid_string_or_size, concat("byte after 'C' must be in range 0x00..0x7F; last byte: 0x", get_token_string()), "char");
return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", "");
}
string_t s(1, static_cast<typename string_t::value_type>(current));
return sax->string(s);
@@ -3141,8 +3137,7 @@ class binary_reader
if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F')
&& size_and_type.first > max_valueless_container_size))
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large,
exception_message("excessive array size", "size"), nullptr));
return out_of_range_error(exception_id::container_too_large, "excessive array size", "size");
}
if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second)))
@@ -3854,7 +3849,7 @@ class binary_reader
{
// in case of failure, advance position by 1 to report the failing location
++chars_read;
sax->parse_error(chars_read, "<end of file>", parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr));
static_cast<void>(report_eof(context));
return false;
}
return true;
@@ -4007,8 +4002,7 @@ class binary_reader
const auto result = static_cast<number_float_t>(number);
if (JSON_HEDLEY_UNLIKELY(std::isfinite(number) && !std::isfinite(result)))
{
return sax->parse_error(chars_read, get_token_string(),
out_of_range::create(exception_id::number_overflow, exception_message("number overflow", "value"), nullptr));
return out_of_range_error(exception_id::number_overflow, "number overflow", "value");
}
return sax->number_float(result, "");
}
@@ -4223,22 +4217,82 @@ class binary_reader
{
if (JSON_HEDLEY_UNLIKELY(current == char_traits<char_type>::eof()))
{
return sax->parse_error(chars_read, "<end of file>",
parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr));
return report_eof(context);
}
return true;
}
/*!
@brief reports an unexpected end of input (out of line to keep the exception
construction out of the hot reading functions)
@param[in] context further context information (for diagnostics)
@return the result of the SAX parser's parse_error()
*/
JSON_HEDLEY_NON_NULL(2)
JSON_HEDLEY_NEVER_INLINE
bool report_eof(const char* context) const
{
return sax->parse_error(chars_read, "<end of file>",
parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr));
}
/*!
@brief reports an out_of_range error at the last read byte (out of line)
*/
JSON_HEDLEY_NEVER_INLINE
bool out_of_range_error(const exception_id id_, const char* detail, const char* context) const
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(id_, exception_message(detail, context), nullptr));
}
JSON_HEDLEY_NEVER_INLINE
bool container_size_error(const char* what) const
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large,
exception_message(concat("excessive ", what, " size"), "size"), nullptr));
}
JSON_HEDLEY_NEVER_INLINE
bool optimized_type_error() const
{
return last_byte_error(exception_id::unexpected_byte, concat("marker 0x", get_token_string(), " is not a permitted optimized array type"), "type");
}
template<typename NumberType>
JSON_HEDLEY_NEVER_INLINE
bool bson_length_error(const char* prefix, const NumberType len, const char* context) const
{
return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(len)), context);
}
JSON_HEDLEY_NEVER_INLINE
bool bson_document_size_error(const std::int32_t document_size, const std::size_t bytes_read) const
{
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document");
}
JSON_HEDLEY_NEVER_INLINE
bool bson_unsupported_type_error(const std::size_t position, const char element_type) const
{
const std::string cr_str = hex_byte(static_cast<std::uint8_t>(element_type));
return sax->parse_error(position, cr_str,
parse_error::create(exception_id::bson_unsupported_type, position, concat("Unsupported BSON record type 0x", cr_str), nullptr));
}
/*!
@brief reports a parse error at the last read byte
@param[in] id_ the id of the parse_error exception
@param[in] detail a detailed error message
@param[in] context further context information
@param[in] tail optional text appended to the detail, followed by "; last byte: 0x<token>"
@return the result of the SAX parser's parse_error()
*/
bool last_byte_error(const exception_id id_, const std::string& detail, const std::string& context) const
JSON_HEDLEY_NEVER_INLINE
bool last_byte_error(const exception_id id_, const std::string& detail, const char* context, const char* tail = nullptr) const
{
return sax->parse_error(chars_read, get_token_string(), parse_error::create(id_, chars_read, exception_message(detail, context), nullptr));
// with a tail, the message reads "<detail><tail>; last byte: 0x<token>"
return sax->parse_error(chars_read, get_token_string(), parse_error::create(id_, chars_read,
exception_message(tail == nullptr ? detail : concat(detail, tail, "; last byte: 0x", get_token_string()), context), nullptr));
}
/*!
@@ -4247,7 +4301,8 @@ class binary_reader
@param[in] context further context information
@return the result of the SAX parser's parse_error()
*/
bool unexpected_byte(const char* detail, const std::string& context) const
JSON_HEDLEY_NEVER_INLINE
bool unexpected_byte(const char* detail, const char* context) const
{
return last_byte_error(exception_id::unexpected_byte, concat(detail, ": 0x", get_token_string()), context);
}
@@ -4258,6 +4313,7 @@ class binary_reader
@param[in] context further context information
@return the result of the SAX parser's parse_error()
*/
JSON_HEDLEY_NEVER_INLINE
bool length_type_error(const char* position, const char* context) const
{
const char* types = input_format == input_format_t::bjdata ? "U, i, u, I, m, l, M, L" : "U, i, I, l, L";
+293 -11
View File
@@ -14,10 +14,12 @@
#include <cstdint> // uint32_t
#include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <limits> // numeric_limits
#include <streambuf> // streambuf
#include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
#include <utility> // pair, declval
#include <vector> // vector
#ifndef JSON_NO_IO
#include <cstdio> // FILE *
@@ -30,6 +32,24 @@
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/string_utils.hpp>
#ifndef JSON_NO_IO
// Which unlocked stdio functions file_input_adapter uses (see
// JSON_DISABLE_UNLOCKED_FILE_IO): 1 = POSIX flockfile/getc_unlocked,
// 2 = Microsoft CRT _lock_file/_fgetc_nolock, 0 = none (std::fgetc, which
// locks the file for every character). Only C libraries known to declare
// the functions are listed; the macros are tested after <cstdio>, which
// defines __GLIBC__.
#if JSON_DISABLE_UNLOCKED_FILE_IO
#define JSON_UNLOCKED_FILE_IO 0
#elif defined(_MSC_VER)
#define JSON_UNLOCKED_FILE_IO 2
#elif defined(__GLIBC__) || defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
#define JSON_UNLOCKED_FILE_IO 1
#else
#define JSON_UNLOCKED_FILE_IO 0
#endif
#endif // JSON_NO_IO
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
@@ -56,18 +76,38 @@ class file_input_adapter
: m_file(f)
{
JSON_ASSERT(m_file != nullptr);
lock();
}
// make class move-only
file_input_adapter(const file_input_adapter&) = delete;
file_input_adapter(file_input_adapter&&) noexcept = default;
file_input_adapter(file_input_adapter&& rhs) noexcept
: m_file(rhs.m_file)
{
// the lock is owned by the thread, not the adapter: hand it over
rhs.m_file = nullptr;
}
file_input_adapter& operator=(const file_input_adapter&) = delete;
file_input_adapter& operator=(file_input_adapter&&) = delete;
~file_input_adapter() = default;
~file_input_adapter()
{
if (m_file != nullptr)
{
unlock();
}
}
std::char_traits<char>::int_type get_character() noexcept
{
// the file is locked for the lifetime of the adapter, so the
// per-character lock of std::fgetc can be skipped
#if JSON_UNLOCKED_FILE_IO == 1
return getc_unlocked(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
return _fgetc_nolock(m_file);
#else
return std::fgetc(m_file);
#endif
}
// returns the number of characters successfully read
@@ -78,6 +118,27 @@ class file_input_adapter
}
private:
// Lock the file once for the whole parse instead of once per character.
// The lock is recursive, so fread() and the user's own stdio calls on the
// same thread keep working.
void lock() noexcept
{
#if JSON_UNLOCKED_FILE_IO == 1
flockfile(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
_lock_file(m_file);
#endif
}
void unlock() noexcept
{
#if JSON_UNLOCKED_FILE_IO == 1
funlockfile(m_file);
#elif JSON_UNLOCKED_FILE_IO == 2
_unlock_file(m_file);
#endif
}
/// the file pointer to read from
std::FILE* m_file;
};
@@ -127,15 +188,26 @@ class input_stream_adapter
input_stream_adapter& operator=(input_stream_adapter&) = delete;
input_stream_adapter& operator=(input_stream_adapter&&) = delete;
#if JSON_PRECISE_STREAM_POSITION
input_stream_adapter(input_stream_adapter&& rhs) noexcept
: is(rhs.is), sb(rhs.sb), lookahead(rhs.lookahead)
: is(rhs.is), sb(rhs.sb)
#if JSON_PRECISE_STREAM_POSITION
, lookahead(rhs.lookahead)
#endif
#if !JSON_DISABLE_STREAM_BULK_SCAN
, token_carry(std::move(rhs.token_carry))
, token_segment(rhs.token_segment)
, current_in_get_area(rhs.current_in_get_area)
, tracking_token(rhs.tracking_token)
#endif
{
rhs.is = nullptr;
rhs.sb = nullptr;
#if JSON_PRECISE_STREAM_POSITION
rhs.lookahead = false;
#endif
}
#if JSON_PRECISE_STREAM_POSITION
// Whether the character last returned by get_character() can be given back
// to the input with release_lookahead().
static constexpr bool supports_lookahead = true;
@@ -155,17 +227,41 @@ class input_stream_adapter
sb->sbumpc();
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
// sgetc() refills the get area if it is exhausted
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment();
}
#endif
auto res = sb->sgetc();
// set eof manually, as we don't use the istream interface.
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
{
// there is nothing to step over next time
lookahead = false;
#if !JSON_DISABLE_STREAM_BULK_SCAN
current_in_get_area = false;
#endif
is->clear(is->rdstate() | std::ios::eofbit);
}
else
{
lookahead = true;
#if !JSON_DISABLE_STREAM_BULK_SCAN
if (JSON_HEDLEY_UNLIKELY(refill))
{
// the peeked character is at gptr(), unless the streambuf has
// no get area
after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_end(sb));
}
else
{
current_in_get_area = true;
}
#endif
}
return res;
}
@@ -179,13 +275,6 @@ class input_stream_adapter
lookahead = false;
}
#else
input_stream_adapter(input_stream_adapter&& rhs) noexcept
: is(rhs.is), sb(rhs.sb)
{
rhs.is = nullptr;
rhs.sb = nullptr;
}
// std::istream/std::streambuf use std::char_traits<char>::to_int_type, to
// ensure that std::char_traits<char>::eof() and the character 0xFF do not
// end up as the same value, e.g., 0xFFFFFFFF.
@@ -194,12 +283,37 @@ class input_stream_adapter
// stays consumed after parsing; see JSON_PRECISE_STREAM_POSITION.
std::char_traits<char>::int_type get_character()
{
#if !JSON_DISABLE_STREAM_BULK_SCAN
// sbumpc() refills the get area if it is exhausted
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
if (JSON_HEDLEY_UNLIKELY(refill))
{
save_token_segment();
}
#endif
auto res = sb->sbumpc();
// set eof manually, as we don't use the istream interface.
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
{
#if !JSON_DISABLE_STREAM_BULK_SCAN
current_in_get_area = false;
#endif
is->clear(is->rdstate() | std::ios::eofbit);
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
else if (JSON_HEDLEY_UNLIKELY(refill))
{
// the consumed character is at gptr() - 1, unless the streambuf
// has no get area and returned it from uflow()
const char* const next = streambuf_access::get_ptr(sb);
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
}
else
{
current_in_get_area = true;
}
#endif
return res;
}
#endif
@@ -218,7 +332,165 @@ class input_stream_adapter
return res;
}
#if !JSON_DISABLE_STREAM_BULK_SCAN
// The lexer may scan the characters in the get area of the streambuf
// directly (SWAR fast paths). The get area is only a window into the
// input: it ends wherever the streambuf's buffer ends, and is refilled by
// the next get_character(). The lexer therefore keeps counting the
// position itself and treats the end of the window as "unknown", not as
// the end of the input.
static constexpr bool supports_bulk_scan = true;
static constexpr bool bulk_window = true;
// Pointer to the next unread character; only valid when
// bulk_remaining() > 0. The character before it is the one last returned
// by get_character().
const char* bulk_data() const
{
return current_end();
}
// Number of unread characters in the get area.
std::size_t bulk_remaining() const
{
if (!current_in_get_area)
{
return 0;
}
return static_cast<std::size_t>(streambuf_access::get_end(sb) - current_end());
}
// Consume @a n characters previously inspected via bulk_data(); the last
// of them becomes the character last returned by get_character().
void bulk_skip(std::size_t n)
{
JSON_ASSERT(n <= bulk_remaining());
// gbump() takes an int, but a get area (e.g., of a std::stringbuf) can
// be larger
constexpr auto max_bump = static_cast<std::size_t>((std::numeric_limits<int>::max)());
for (; n > max_bump; n -= max_bump)
{
streambuf_access::bump(sb, static_cast<int>(max_bump));
}
streambuf_access::bump(sb, static_cast<int>(n));
}
// The adapter keeps the current token for the lexer's error messages: the
// part in the get area is only remembered as a pointer, and copied out
// when the get area is about to be refilled.
static constexpr bool supports_token_tracking = true;
// Start a token with the characters read from now on. Until this is
// called, nothing is tracked (the binary readers never call it).
void begin_token() noexcept
{
tracking_token = true;
token_carry.clear();
token_segment = current_end();
}
// Start a new token with the character @a c last returned by
// get_character().
void mark_token_start(std::char_traits<char>::int_type c)
{
tracking_token = true;
token_carry.clear();
if (current_in_get_area)
{
token_segment = current_end() - 1;
}
else
{
token_segment = nullptr;
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
// Copy the characters read since mark_token_start() to @a out, without the
// last one if @a drop_last.
void copy_token(std::vector<char>& out, bool drop_last) const
{
out = token_carry;
if (token_segment != nullptr)
{
out.insert(out.end(), token_segment, current_end());
}
if (drop_last && !out.empty())
{
out.pop_back();
}
}
#endif
private:
#if !JSON_DISABLE_STREAM_BULK_SCAN
// Reads the protected get-area pointers of any std::streambuf. Naming the
// members through a derived class makes forming the member pointers
// accessible; calling them on another streambuf object is then ordinary
// member access through a pointer to member.
struct streambuf_access : std::streambuf
{
static const char* get_ptr(std::streambuf* s)
{
return (s->*(&streambuf_access::gptr))();
}
static const char* get_end(std::streambuf* s)
{
return (s->*(&streambuf_access::egptr))();
}
static const char* get_begin(std::streambuf* s)
{
return (s->*(&streambuf_access::eback))();
}
static void bump(std::streambuf* s, int n)
{
(s->*(&streambuf_access::gbump))(n);
}
};
// one past the character last returned by get_character(), if that is in
// the get area
const char* current_end() const
{
#if JSON_PRECISE_STREAM_POSITION
// the character is peeked at gptr() and not stepped over yet, unless
// it was given back with release_lookahead()
return streambuf_access::get_ptr(sb) + (lookahead ? 1 : 0);
#else
return streambuf_access::get_ptr(sb);
#endif
}
// the get area is about to be replaced: copy the token's part in it
void save_token_segment()
{
if (token_segment != nullptr)
{
token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb));
token_segment = nullptr;
}
}
// character @a c was just read into a new get area (@a in_get_area) or
// returned without one
void after_refill(std::char_traits<char>::int_type c, bool in_get_area)
{
current_in_get_area = in_get_area;
if (!tracking_token)
{
return;
}
if (in_get_area)
{
token_segment = current_end() - 1;
}
else
{
token_carry.push_back(std::char_traits<char>::to_char_type(c));
}
}
#endif
#if JSON_PRECISE_STREAM_POSITION
// Step over the character last returned by get_character(). The character
// has already been peeked successfully, so for every streambuf with a get
@@ -240,6 +512,16 @@ class input_stream_adapter
/// whether get_character() peeked a character that is not consumed yet
bool lookahead = false;
#endif
#if !JSON_DISABLE_STREAM_BULK_SCAN
/// the part of the current token that was in earlier get areas
std::vector<char> token_carry{}; // NOLINT(readability-redundant-member-init)
/// start of the current token's part in the get area, or nullptr
const char* token_segment = nullptr;
/// whether the character last returned by get_character() is in the get area
bool current_in_get_area = false;
/// whether a token is tracked at all; the binary readers never start one
bool tracking_token = false;
#endif
};
#endif // JSON_NO_IO
+14 -4
View File
@@ -187,12 +187,19 @@ Shared by json_sax_dom_parser and json_sax_dom_callback_parser.
@param[in] ref the object or array that was just created
@return whether parsing should continue (false after reporting out_of_range.408)
*/
template<typename SAX, typename BasicJsonType>
JSON_HEDLEY_NEVER_INLINE
bool container_too_large_error(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref)
{
return sax.parse_error(0, "", out_of_range::create(exception_id::container_too_large, concat("excessive ", kind, " size: ", std::to_string(len)), ref));
}
template<typename SAX, typename BasicJsonType>
bool check_container_size(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref)
{
if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref->max_size()))
{
return sax.parse_error(0, "", out_of_range::create(exception_id::container_too_large, concat("excessive ", kind, " size: ", std::to_string(len)), ref));
return container_too_large_error(sax, len, kind, ref);
}
return true;
}
@@ -674,16 +681,19 @@ class json_sax_dom_callback_parser
if (keep && ref_stack.back())
{
auto& obj = *ref_stack.back()->m_data.m_value.object;
const auto it = obj.find(val);
if (it != obj.end())
// a single lookup: insert the placeholder, or find the existing member
const auto inserted = obj.emplace(val, discarded);
const auto it = inserted.first;
if (!inserted.second)
{
// this is a duplicate key (legal in JSON); remember its
// current value so it can be restored later if the new
// value is rejected by the callback, instead of being
// erased together with the discarded placeholder
duplicate_key_stash.emplace_back(&(it->second), it->second);
it->second = discarded;
}
object_element = &(obj[val] = discarded);
object_element = &(it->second);
}
return true;
+278 -34
View File
@@ -166,6 +166,43 @@ constexpr bool input_adapter_supports_bulk_scan(std::false_type /*detected*/)
return false;
}
// Detect whether an input adapter keeps the current token itself and can
// hand it out on demand (see input_stream_adapter::supports_token_tracking),
// so the lexer need not copy every scanned character into token_string.
template<typename InputAdapterType>
using detect_supports_token_tracking = decltype(InputAdapterType::supports_token_tracking);
template<typename InputAdapterType>
constexpr bool input_adapter_supports_token_tracking(std::true_type /*detected*/)
{
return InputAdapterType::supports_token_tracking;
}
template<typename InputAdapterType>
constexpr bool input_adapter_supports_token_tracking(std::false_type /*detected*/)
{
return false;
}
// Detect whether the contiguous block of a bulk-scan adapter is only a window
// into the input (see input_stream_adapter::bulk_window): its end is not the
// end of the input, so a token that touches it must be finished character by
// character.
template<typename InputAdapterType>
using detect_bulk_window = decltype(InputAdapterType::bulk_window);
template<typename InputAdapterType>
constexpr bool input_adapter_bulk_window(std::true_type /*detected*/)
{
return InputAdapterType::bulk_window;
}
template<typename InputAdapterType>
constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
{
return false;
}
/*!
@brief lexical analysis
@@ -184,9 +221,20 @@ class lexer : public lexer_base<BasicJsonType>
/// whether the last read token can be reconstructed from the input adapter
/// on demand (in error paths) instead of being copied on every scanned
/// character; see input_adapter_supports_seek
static constexpr bool lazy_token_string =
static constexpr bool seekable =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the adapter keeps the token itself (mark_token_start() /
/// copy_token()); see input_adapter_supports_token_tracking
static constexpr bool adapter_token =
!seekable && input_adapter_supports_token_tracking<InputAdapterType>(is_detected<detect_supports_token_tracking, InputAdapterType> {});
/// whether the last read token is obtained from the input adapter on
/// demand (in error paths) instead of being copied on every scanned
/// character: seekable adapters reconstruct it from the consumed input,
/// token-tracking adapters keep it themselves
static constexpr bool lazy_token_string = seekable || adapter_token;
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
@@ -194,7 +242,7 @@ class lexer : public lexer_base<BasicJsonType>
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
static constexpr bool lazy_position = seekable;
/// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see
@@ -203,14 +251,17 @@ class lexer : public lexer_base<BasicJsonType>
input_adapter_supports_lookahead<InputAdapterType>(is_detected<detect_supports_lookahead, InputAdapterType> {});
/// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
/// directly from a contiguous input buffer (SWAR fast path). Without
/// lazy_token_string / lazy_position, the bulk paths copy the consumed
/// characters into token_string and advance the position counters
/// themselves (see bulk_consume).
static constexpr bool bulk_scan =
lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
/// whether the contiguous block is only a window into the input, so that
/// its end does not end a token; see input_adapter_bulk_window
static constexpr bool bulk_window =
input_adapter_bulk_window<InputAdapterType>(is_detected<detect_bulk_window, InputAdapterType> {});
public:
using token_type = typename lexer_base<BasicJsonType>::token_type;
@@ -219,7 +270,11 @@ class lexer : public lexer_base<BasicJsonType>
: ia(std::move(adapter))
, ignore_comments(ignore_comments_)
, discard_number_values(discard_number_values_)
{}
{
// the token string of an error before the first token is everything
// read so far
begin_token(std::integral_constant<bool, adapter_token> {});
}
// deleted because of pointer members
lexer(const lexer&) = delete;
@@ -236,8 +291,8 @@ class lexer : public lexer_base<BasicJsonType>
/// contiguous input: try to decode the 4 hex digits following `\\u`
/// directly from the input buffer via hex_codepoint(), instead of 4 calls
/// to get(). On success, advances the adapter exactly as those 4 get()
/// calls would (bulk_scan implies lazy_position, so there are no position
/// counters to advance) and leaves @a current holding the last of
/// calls would (including the position and token bookkeeping of a
/// streaming adapter, see bulk_consume) and leaves @a current holding the last of
/// the 4 digits, just as the last such get() would; the codepoint is
/// written to @a out. Makes no state change and returns false for a
/// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not
@@ -256,7 +311,7 @@ class lexer : public lexer_base<BasicJsonType>
{
return false;
}
ia.bulk_skip(4);
bulk_consume(reinterpret_cast<const char*>(raw), 4);
current = char_traits<char_type>::to_int_type(raw[3]);
out = codepoint;
return true;
@@ -385,7 +440,36 @@ class lexer : public lexer_base<BasicJsonType>
return;
}
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(pos);
bulk_consume(reinterpret_cast<const char*>(data), pos);
}
/// consume @a n characters at @a data (the adapter's bulk_data()) and do
/// the bookkeeping get() would have done for them; the characters are
/// never newlines
void bulk_consume(const char* data, std::size_t n)
{
bulk_track_position(n, std::integral_constant<bool, lazy_position> {});
bulk_track_token(data, n, std::integral_constant<bool, lazy_token_string> {});
ia.bulk_skip(n);
}
/// seekable adapter: the position is derived from the adapter
void bulk_track_position(std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the characters
void bulk_track_position(std::size_t n, std::false_type /*lazy*/) noexcept
{
position.chars_read_total += n;
position.chars_read_current_line += n;
}
/// the token is obtained from the adapter
void bulk_track_token(const char* /*data*/, std::size_t /*n*/, std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: capture the characters
void bulk_track_token(const char* data, std::size_t n, std::false_type /*lazy*/)
{
token_string.insert(token_string.end(), data, data + n);
}
/// streaming input: no bulk fast path
@@ -1541,9 +1625,12 @@ scan_number_done:
const std::size_t avail = rem + 1;
// validate + classify the number extent (mirrors scan_number()'s grammar)
// and accumulate the mantissa digits on the way, so that tokens with up
// to 19 digits need no second pass for their value
std::size_t i = 0;
std::size_t dot_index = std::string::npos;
token_type number_type = token_type::value_unsigned;
std::uint64_t w = 0;
if (data[0] == '-')
{
number_type = token_type::value_integer;
@@ -1553,22 +1640,21 @@ scan_number_done:
return token_type::uninitialized;
}
}
const std::size_t integer_begin = i;
if (data[i] == '0')
{
++i;
}
else if (data[i] >= '1' && data[i] <= '9')
{
++i;
while (i < avail && data[i] >= '0' && data[i] <= '9')
{
++i;
}
i = accumulate_digits(data, i, avail, w);
}
else
{
return token_type::uninitialized;
}
std::size_t digit_count = i - integer_begin;
std::size_t fraction_digits = 0;
if (i < avail && data[i] == '.')
{
number_type = token_type::value_float;
@@ -1578,31 +1664,45 @@ scan_number_done:
{
return token_type::uninitialized;
}
while (i < avail && data[i] >= '0' && data[i] <= '9')
{
++i;
}
const std::size_t fraction_begin = i;
i = accumulate_digits(data, i, avail, w);
fraction_digits = i - fraction_begin;
digit_count += fraction_digits;
}
// the mantissa ends here, whether or not an exponent part follows
const std::size_t mantissa_end = i;
std::int64_t exponent = 0;
if (i < avail && (data[i] == 'e' || data[i] == 'E'))
{
number_type = token_type::value_float;
++i;
bool negative_exponent = false;
if (i < avail && (data[i] == '+' || data[i] == '-'))
{
negative_exponent = data[i] == '-';
++i;
}
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
{
return token_type::uninitialized;
}
// saturated far beyond every range, see parse_float_exponent()
constexpr std::int64_t saturation = 100000000000000000; // 10^17
while (i < avail && data[i] >= '0' && data[i] <= '9')
{
if (exponent < saturation)
{
exponent = (exponent * 10) + (data[i] - '0');
}
++i;
}
exponent = negative_exponent ? -exponent : exponent;
}
const std::size_t len = i;
if (number_may_continue(len == avail, std::integral_constant<bool, bulk_window> {}))
{
return token_type::uninitialized;
}
// reset() records where this token starts (for diagnostics), so it has
// to run before the input position advances below
@@ -1617,10 +1717,14 @@ scan_number_done:
#if !JSON_DIAGNOSTIC_POSITIONS
if (number_type != token_type::value_float)
{
const token_type integer_result = convert_integer(number_type, data, data + len);
// up to 18 digits always fit into std::uint64_t and std::int64_t
const token_type integer_result = (digit_count <= 18)
? store_integer(number_type, w)
: convert_integer(number_type, data, data + len);
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{
ia.bulk_skip(len - 1);
bulk_consume(data + 1, len - 1);
read_number_terminator(std::integral_constant<bool, bulk_window> {});
return integer_result;
}
// The value does not fit an integer, so this token converts as a
@@ -1630,17 +1734,99 @@ scan_number_done:
}
#endif
decimal_point_position = dot_index;
bulk_consume(data + 1, len - 1);
read_number_terminator(std::integral_constant<bool, bulk_window> {});
// a float whose digits all fit into w converts from the accumulated
// value; longer ones (and other float formats) take the general path
if (number_type == token_type::value_float && digit_count <= 19)
{
float_significand s;
s.w = w;
s.exponent = exponent - static_cast<std::int64_t>(fraction_digits);
s.negative = data[0] == '-';
if (convert_accumulated_float(s, std::integral_constant<bool, has_native_float_format<number_float_t>::value> {}))
{
// the token text is only copied into token_buffer when
// get_string() asks for it; the library's own SAX handlers
// ignore it. A window into a stream may be refilled by the
// next read, so there the text is copied right away.
if (bulk_window)
{
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
}
else
{
pending_number_text = data;
pending_number_length = len;
}
return token_type::value_float;
}
}
// materialize the token exactly as scan_number() would. reset() already
// cleared token_buffer, so append() fills it (assign() is avoided
// because custom string_t types need not provide it)
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
decimal_point_position = dot_index;
ia.bulk_skip(len - 1);
return convert_number(number_type, mantissa_end);
}
/// window adapter: a number that reaches the end of the window may continue
/// after it, so scan_number() has to read it character by character
static constexpr bool number_may_continue(bool at_end, std::true_type /*window*/) noexcept
{
return at_end;
}
/// the end of the block is the end of the input, which ends the number
static constexpr bool number_may_continue(bool /*at_end*/, std::false_type /*window*/) noexcept
{
return false;
}
/// window adapter: read the character after a bulk-scanned number and unget
/// it, as scan_number() does. The adapter is a stream that is handed back to
/// the user, who must find the same input consumed either way.
void read_number_terminator(std::true_type /*window*/)
{
get();
unget();
}
/// the input is not handed back, so the terminator can stay unread
void read_number_terminator(std::false_type /*window*/) const noexcept {}
/// binary32/binary64: the value of a float token from its accumulated
/// significand (see scan_number_bulk_contiguous())
bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept
{
value_float = decimal_to_float<number_float_t>(s);
return true;
}
/// other formats (e.g. x87 long double): not instantiated for them, as
/// decimal_to_float() only supports binary32/binary64
static bool convert_accumulated_float(const float_significand& /*s*/, std::false_type /*native*/) noexcept
{
return false;
}
/// the value of an integer token with at most 18 digits, accumulated in @a w
token_type store_integer(token_type number_type, std::uint64_t w) noexcept
{
if (number_type == token_type::value_unsigned)
{
value_unsigned = static_cast<number_unsigned_t>(w);
// reject values that do not round-trip into a narrower type
return (static_cast<std::uint64_t>(value_unsigned) == w) ? token_type::value_unsigned : token_type::uninitialized;
}
const std::int64_t x = -static_cast<std::int64_t>(w);
value_integer = static_cast<number_integer_t>(x);
return (static_cast<std::int64_t>(value_integer) == x) ? token_type::value_integer : token_type::uninitialized;
}
/// contiguous input: try the number fast path, else the byte-path scanner
token_type scan_number_dispatch(std::true_type /*bulk*/)
{
@@ -1683,6 +1869,7 @@ scan_number_done:
void reset() noexcept
{
token_buffer.clear();
pending_number_text = nullptr;
decimal_point_position = std::string::npos;
#if JSON_DIAGNOSTIC_POSITIONS
@@ -1696,11 +1883,30 @@ scan_number_done:
/// seekable adapter: remember where the current token starts so it can be
/// reconstructed from the input on error; current has already been
/// consumed, hence the -1
void note_token_start(std::true_type /*lazy*/) noexcept
void note_token_start(std::true_type /*lazy*/)
{
note_token_start_lazy(std::integral_constant<bool, adapter_token> {});
}
void note_token_start_lazy(std::false_type /*adapter_token*/) noexcept
{
token_string_start = ia.get_consumed_count() - 1;
}
/// token-tracking adapter: start tracking the token at the first character
void begin_token(std::true_type /*adapter_token*/) noexcept
{
ia.begin_token();
}
void begin_token(std::false_type /*adapter_token*/) const noexcept {}
/// token-tracking adapter: the token starts with the current character
void note_token_start_lazy(std::true_type /*adapter_token*/)
{
ia.mark_token_start(current);
}
/// streaming adapter: start copying the token eagerly, beginning with the
/// already-read first character
void note_token_start(std::false_type /*lazy*/) noexcept
@@ -1923,10 +2129,30 @@ scan_number_done:
/// return current string value
string_t& get_string()
{
if (pending_number_text != nullptr)
{
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(pending_number_text), pending_number_length);
pending_number_text = nullptr;
}
// a number token holds '.' regardless of the locale (#4084)
return token_buffer;
}
/// the value of a string token (key or string), which never has pending
/// number text, so get_string()'s check is not needed
string_t& get_string_token() noexcept
{
JSON_ASSERT(pending_number_text == nullptr);
return token_buffer;
}
/// the text of a float token for a SAX handler that ignores it (the
/// library's own handlers): empty, without copying the token
const string_t& get_ignored_number_text() const noexcept
{
return token_buffer;
}
/////////////////////
// diagnostics
/////////////////////
@@ -2021,6 +2247,20 @@ scan_number_done:
public:
/// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{
return collect_token_chars_lazy(out, std::integral_constant<bool, adapter_token> {});
}
/// token-tracking adapter: the adapter hands out everything read since
/// mark_token_start(), except a pending unget (see below)
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::true_type /*adapter_token*/) const
{
const bool pending_real_unget = next_unget && current != char_traits<char_type>::eof();
ia.copy_token(out, pending_real_unget);
return out;
}
const std::vector<char_type>& collect_token_chars_lazy(std::vector<char_type>& out, std::false_type /*adapter_token*/) const
{
// a pending unget of a real (non-EOF) character means that character
// was consumed from the input but is not part of the token; EOF is
@@ -2142,9 +2382,8 @@ scan_number_done:
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
other whitespace are left to that loop. Skipping bypasses get();
bulk_consume() does its bookkeeping.
*/
void skip_indentation(std::true_type /*bulk*/)
{
@@ -2167,7 +2406,7 @@ scan_number_done:
break;
}
}
ia.bulk_skip(pos);
bulk_consume(data, pos);
}
/// streaming input: no bulk fast path
@@ -2309,6 +2548,11 @@ scan_number_done:
/// whether the next get() call should just return current
bool next_unget = false;
/// the text of the current float token in the input, not yet copied into
/// token_buffer (see get_string()), or nullptr
const char* pending_number_text = nullptr;
std::size_t pending_number_length = 0;
/// the start position of the current token
position_t position {};
+66 -10
View File
@@ -19,7 +19,7 @@
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <string> // string
#include <type_traits> // conditional, integral_constant, true_type, false_type
#include <type_traits> // conditional, integral_constant, is_signed, true_type, false_type
#include <utility> // move
#include <nlohmann/detail/bit_ops.hpp>
@@ -50,6 +50,20 @@ NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
// whether an integer of a signed type is negative; the overload for other types
// avoids comparing an unsigned value with zero
template<typename NumberType>
constexpr bool is_negative_integer(NumberType x, std::true_type /*is_signed*/) noexcept
{
return x < 0;
}
template<typename NumberType>
constexpr bool is_negative_integer(NumberType /*unused*/, std::false_type /*is_signed*/) noexcept
{
return false;
}
/*!
@brief fast integer parser for an already-validated unsigned integer
@@ -83,8 +97,10 @@ bool parse_integer_unsigned(const char* first, const char* last, NumberUnsignedT
x = (x * 10u) + digit;
}
value = static_cast<NumberUnsignedType>(x);
// reject values that do not round-trip into a narrower NumberUnsignedType
return static_cast<std::uint64_t>(value) == x;
// reject values that do not round-trip into a narrower NumberUnsignedType,
// and values that wrap around to negative numbers if it is a signed type
// (number_unsigned_t may be the same type as number_integer_t, #2573)
return static_cast<std::uint64_t>(value) == x && !is_negative_integer(value, std::is_signed<NumberUnsignedType>());
}
/*!
@@ -259,13 +275,53 @@ JSON_HEDLEY_ALWAYS_INLINE std::uint32_t parse_eight_digits(std::uint64_t v) noex
return static_cast<std::uint32_t>(((v & 0x0000FFFF0000FFFFu) * 42949672960001u) >> 32u);
}
/// whether [first, last) contains a digit other than '0'
inline bool has_nonzero_digit(const char* first, const char* last) noexcept
/// whether the eight bytes in @a v (see read_eight_bytes()) are all ASCII
/// digits (after fast_float)
JSON_HEDLEY_ALWAYS_INLINE bool is_eight_digits(std::uint64_t v) noexcept
{
return std::any_of(first, last, [](char c) noexcept
return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u;
}
/*!
@brief skip the ASCII digits starting at data[i] and accumulate their value
Eight digits are taken at a time while at least eight bytes remain. @a w wraps
around after 19 digits; the caller only uses it for shorter digit runs.
@return the index of the first byte at or after @a i that is not a digit
*/
JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::size_t i, std::size_t avail, std::uint64_t& w) noexcept
{
while (avail - i >= 8)
{
return c != '0';
});
const std::uint64_t v = read_eight_bytes(data + i);
if (!is_eight_digits(v))
{
break;
}
w = (w * 100000000u) + parse_eight_digits(v);
i += 8;
}
while (i < avail && data[i] >= '0' && data[i] <= '9')
{
w = (w * 10u) + static_cast<std::uint64_t>(data[i] - '0');
++i;
}
return i;
}
/// whether [first, last) contains a digit other than '0'; a plain loop, as
/// GCC calls std::any_of out of line even for the usual empty range
JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept
{
for (; first != last; ++first)
{
if (*first != '0')
{
return true;
}
}
return false;
}
/// the value of the validated exponent digits [+-]?[0-9]+ in [first, last),
@@ -311,7 +367,7 @@ truncated is set.
std::string::npos if there is none
@param[in] mantissa_end index of the 'e'/'E', or the token length
*/
inline float_significand parse_float_significand(const char* first, const char* last,
JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char* first, const char* last,
std::size_t decimal_point_position, std::size_t mantissa_end) noexcept
{
float_significand s;
@@ -704,7 +760,7 @@ inline void float_from_bits(std::uint64_t bits, float& value) noexcept
{
static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits");
const auto bits32 = static_cast<std::uint32_t>(bits);
std::memcpy(&value, &bits32, sizeof(value));
std::memcpy(&value, &bits32, sizeof(bits32));
}
/// the powers of ten that are exact in binary64 (up to 10^22)
+35 -4
View File
@@ -51,6 +51,20 @@ template<typename BasicJsonType>
using parser_callback_t =
std::function<bool(int /*depth*/, parse_event_t /*event*/, BasicJsonType& /*parsed*/)>;
/// whether a SAX handler ignores the text of float tokens (the library's own
/// handlers do), so that the parser need not materialize it
template<typename SAX>
struct sax_ignores_number_text : std::false_type {};
template<typename BasicJsonType, typename InputAdapterType>
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
template<typename BasicJsonType, typename InputAdapterType>
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
template<typename BasicJsonType>
struct sax_ignores_number_text<json_sax_acceptor<BasicJsonType>> : std::true_type {};
/*!
@brief syntax analysis
@@ -171,6 +185,23 @@ class parser
}
private:
/// the text of the current float token for @a sax (see sax_ignores_number_text)
template<typename SAX>
const string_t& number_float_text(SAX* /*sax*/)
{
return number_float_text(std::integral_constant<bool, sax_ignores_number_text<SAX>::value> {});
}
const string_t& number_float_text(std::true_type /*ignored*/) const noexcept
{
return m_lexer.get_ignored_number_text();
}
string_t& number_float_text(std::false_type /*ignored*/)
{
return m_lexer.get_string();
}
/*!
@brief run a DOM SAX parser to completion and position the lexer
@@ -247,7 +278,7 @@ class parser
{
return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
}
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
{
return false;
}
@@ -301,7 +332,7 @@ class parser
out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr));
}
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string())))
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax))))
{
return false;
}
@@ -347,7 +378,7 @@ class parser
case token_type::value_string:
{
if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string())))
if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string_token())))
{
return false;
}
@@ -456,7 +487,7 @@ class parser
return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
}
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
{
return false;
}
+314
View File
@@ -0,0 +1,314 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-FileCopyrightText: 2018-2025 The simdjson authors <https://github.com/simdjson/simdjson>
// SPDX-License-Identifier: MIT
#pragma once
#include <array> // array
#include <atomic> // atomic
#include <cstddef> // size_t
#include <cstdint> // uint8_t, uint64_t
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/macro_scope.hpp>
// Vector code for runs of string bytes, used by the lexer for contiguous input
// (see string_bulk_run()). NEON (AArch64) and SSE2 (x86-64) belong to the
// baseline instruction sets and are used by default. The vector UTF-8 check
// needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code must not
// depend on the flags of a translation unit (two translation units with
// different flags would have different definitions of the same inline
// functions): the check is compiled for SSSE3 with a function attribute and
// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the
// portable check. Code compiled for SSSE3 anyway (-mssse3 or more) skips the
// CPU check. JSON_NO_SIMD selects the portable code everywhere.
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
// without disabling the SSE2 code.
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#include <arm_neon.h>
#define JSON_SCAN_NEON 1
#else
#define JSON_SCAN_NEON 0
#endif
// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no <cpuid.h>)
#if !defined(JSON_NO_SIMD) && !JSON_SCAN_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && !defined(_M_ARM64EC) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2))
#include <emmintrin.h>
#define JSON_SCAN_SSE2 1
#else
#define JSON_SCAN_SSE2 0
#endif
#if JSON_SCAN_SSE2 && defined(__SSSE3__)
#include <tmmintrin.h>
#define JSON_SCAN_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
#else
#define JSON_SCAN_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
#endif
#if JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && !defined(NLOHMANN_JSON_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9))))
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3)
#include <cpuid.h>
#include <tmmintrin.h>
#define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
#define JSON_SCAN_SSSE3_TARGET __attribute__((target("ssse3")))
#elif JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && defined(_MSC_VER)
// (MSVC compiles intrinsics of any instruction set)
#include <intrin.h>
#include <tmmintrin.h>
#define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
#define JSON_SCAN_SSSE3_TARGET
#else
#define JSON_SCAN_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
#define JSON_SCAN_SSSE3_TARGET
#endif
#define JSON_SCAN_VECTOR (JSON_SCAN_NEON || JSON_SCAN_SSE2)
#define JSON_SCAN_VECTOR_UTF8 (JSON_SCAN_NEON || JSON_SCAN_SSSE3 || JSON_SCAN_SSSE3_DISPATCH)
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
#if JSON_SCAN_VECTOR
/*!
@brief the first byte of a string run that is a quote, a backslash, a control
character, or not ASCII, 16 bytes per step
Stops at such a byte, or where fewer than 16 bytes are left (the caller tells
the two apart). A signed compare with 0x20 finds control characters and
non-ASCII bytes at once.
*/
JSON_HEDLEY_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept
{
while (e - p >= 16)
{
#if JSON_SCAN_NEON
const uint8x16_t in = vld1q_u8(p);
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)));
// one nibble per byte (the usual NEON replacement of x86's movemask, see
// D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022)
const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
if (bits != 0)
{
return p + (count_trailing_zeros(bits) >> 2u);
}
#else
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)));
const auto bits = static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
if (bits != 0)
{
return p + count_trailing_zeros(bits);
}
#endif
p += 16;
}
return p;
}
#endif
#if JSON_SCAN_VECTOR
/// vector_plain_run() for runs that are already longer than a few words:
/// inline with SSE2, out of line with NEON, where a larger string loop slows
/// down the rest of the lexer
#if JSON_SCAN_NEON
JSON_HEDLEY_NEVER_INLINE inline
#else
JSON_HEDLEY_ALWAYS_INLINE
#endif
const unsigned char* vector_plain_run_long(const unsigned char* p, const unsigned char* e) noexcept
{
return vector_plain_run(p, e);
}
#endif
#if JSON_SCAN_SSSE3_DISPATCH
/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9)
inline bool cpu_ssse3() noexcept
{
#if defined(_MSC_VER) && !defined(__clang__)
std::array<int, 4> regs {{}};
__cpuid(regs.data(), 1);
return (static_cast<unsigned>(regs[2]) & (1u << 9u)) != 0;
#else
unsigned eax = 0;
unsigned ebx = 0;
unsigned ecx = 0;
unsigned edx = 0;
return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0;
#endif
}
/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic
/// that is initialized at compile time, so that neither a guard of a local
/// static nor a global constructor is needed (threads that ask at the same
/// time all store the same answer)
JSON_HEDLEY_ALWAYS_INLINE bool cpu_has_ssse3() noexcept
{
static std::atomic<int> known{0}; // 0: not asked yet, 1: no, 2: yes
int state = known.load(std::memory_order_relaxed);
if (JSON_HEDLEY_UNLIKELY(state == 0))
{
state = cpu_ssse3() ? 2 : 1;
known.store(state, std::memory_order_relaxed);
}
return state == 2;
}
#endif
#if JSON_SCAN_VECTOR_UTF8
/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In
/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each
/// maps a nibble (high and low nibble of the previous byte, high nibble of the
/// current byte) to the errors it allows; a byte pair is ill-formed if all
/// three have an error bit in common.
template<typename Dummy = void>
struct utf8_lookup4
{
static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u;
static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u;
static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts;
static const std::array<std::uint8_t, 16> byte_1_high;
static const std::array<std::uint8_t, 16> byte_1_low;
static const std::array<std::uint8_t, 16> byte_2_high;
};
template<typename Dummy>
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_1_high =
{
{
too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long,
two_conts, two_conts, two_conts, two_conts,
too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4
}
};
template<typename Dummy>
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_1_low =
{
{
carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry,
carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000,
carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000,
carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000
}
};
template<typename Dummy>
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_2_high =
{
{
too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short,
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4),
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | overlong_3 | too_large),
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | surrogate | too_large),
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | surrogate | too_large),
too_short, too_short, too_short, too_short
}
};
/*!
@brief the run of plain string bytes and well-formed UTF-8 at p (a character
boundary), 16 bytes per step
The first quote, backslash, or control character is found with vector
compares, and the UTF-8 check of J. Keiser and D. Lemire covers the bytes up
to it. Returns true with @a block at that byte; returns false with @a block at
the start of the 16 bytes where the check found ill-formed UTF-8 or where
fewer than 16 bytes are left, for the caller to finish one sequence at a time.
Out of line, so that no constants of the check occupy registers in the
caller's loop. On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()).
*/
JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(const unsigned char* p, const unsigned char* e, const unsigned char*& block_out) noexcept
{
using lookup = utf8_lookup4<>;
const unsigned char* block = p;
#if JSON_SCAN_NEON
const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data());
const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data());
const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data());
uint8x16_t prev = vdupq_n_u8(0);
while (e - block >= 16)
{
const uint8x16_t in = vld1q_u8(block);
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20)));
const uint8x16_t prev1 = vextq_u8(prev, in, 15);
const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4)));
const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80)));
const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc);
const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0);
if (special_bits != 0)
{
// errors up to the special byte count (an incomplete sequence
// before a quote shows at the quote); the bytes after it do not
const unsigned k = static_cast<unsigned>(count_trailing_zeros(special_bits)) >> 2u;
const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} :
(std::uint64_t{1} << (4u * (k + 1u))) - 1u;
if ((err_bits & upto) == 0)
{
block_out = block + k;
return true;
}
break;
}
if (err_bits != 0)
{
break;
}
prev = in;
block += 16;
}
#else
// the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit
// shifts, as there are no byte shifts)
const __m128i t1h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void)
const __m128i t1l = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void)
const __m128i t2h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void)
const __m128i nibble = _mm_set1_epi8(0x0F);
const __m128i zero = _mm_setzero_si128();
__m128i prev = zero;
while (e - block >= 16)
{
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(block))); // NOLINT(bugprone-casting-through-void)
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
_mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20
const __m128i prev1 = _mm_alignr_epi8(in, prev, 15);
const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)),
_mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))),
_mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble)));
const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)),
_mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80)));
const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast<char>(-128))), sc);
const auto special_bits = static_cast<unsigned>(_mm_movemask_epi8(special));
const auto err_bits = ~static_cast<unsigned>(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu;
if (special_bits != 0)
{
const auto k = static_cast<unsigned>(count_trailing_zeros(static_cast<std::uint64_t>(special_bits)));
if ((err_bits & ((2u << k) - 1u)) == 0)
{
block_out = block + k;
return true;
}
break;
}
if (err_bits != 0)
{
break;
}
prev = in;
block += 16;
}
#endif
block_out = block;
return false;
}
#endif
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END
+117 -15
View File
@@ -14,6 +14,7 @@
#include <cstring> // memcpy
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/input/simd_scan.hpp>
#include <nlohmann/detail/macro_scope.hpp>
// Optional SIMD backend for bulk UTF-8 validation. This is an opt-in external
@@ -66,7 +67,7 @@ inline std::uint64_t swar_string_special(std::uint64_t v) noexcept
// return the index of the first is_string_special() byte in [data, data+n), or
// n if every byte is ordinary; scans 8 bytes at a time
inline std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept
JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept
{
std::size_t i = 0;
for (; i + 8 <= n; i += 8)
@@ -235,35 +236,136 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n
return pos;
}
// Scalar (C++11) computation of the bulk run length: the number of leading
// bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8
#if JSON_SCAN_VECTOR_UTF8
/// the end of scan_string_vector from block, where the vector loop stopped
/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a
/// time, from the start of a sequence that crosses into the block
inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e) noexcept
{
for (int i = 1; i <= 3 && block - i >= p; ++i)
{
const unsigned char c = block[-i];
if (c < 0x80)
{
break;
}
if (c >= 0xC0)
{
const int len = 2 + static_cast<int>(c >= 0xE0) + static_cast<int>(c >= 0xF0);
if (len > i)
{
block -= i;
}
break;
}
}
for (p = block; p != e;)
{
if (*p < 0x80)
{
if (is_string_special(*p))
{
return p;
}
++p;
continue;
}
const std::size_t n = validate_one_utf8(p, static_cast<std::size_t>(e - p));
if (n == 0)
{
return p;
}
p += n;
}
return p;
}
/// the rest of a string from p (a character boundary), with the vector UTF-8
/// check and the scalar check for the end of the block where it stopped
inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e) noexcept
{
const unsigned char* block = p;
if (vector_utf8_run(p, e, block))
{
return block;
}
return scan_string_finish(p, block, e);
}
#endif
#if JSON_SCAN_VECTOR
/// the end of the run of plain string characters at @a p: the first quote,
/// backslash, control character, or non-ASCII byte, or @a e. The first words
/// are checked with SWAR (one; four with NEON, where the vector mask costs
/// more), as most keys and short values end there; longer runs continue 16
/// bytes at a time.
JSON_HEDLEY_ALWAYS_INLINE const unsigned char* plain_run_end(const unsigned char* p, const unsigned char* e) noexcept
{
constexpr int prefix_words = JSON_SCAN_NEON ? 4 : 1;
for (int words = prefix_words; words > 0 && e - p >= 8; --words)
{
const std::uint64_t special = swar_string_special(read_eight_bytes(p));
if (special != 0)
{
return p + (static_cast<std::size_t>(count_trailing_zeros(special)) / 8);
}
p += 8;
}
p = vector_plain_run_long(p, e);
if (e - p < 16) // else the vector loop stopped at a special byte
{
p += find_string_special(p, static_cast<std::size_t>(e - p));
}
return p;
}
#endif
// Computation of the bulk run length: the number of leading bytes in
// [data, data+n) that are ordinary ASCII or complete well-formed UTF-8
// sequences, stopping before the first byte that needs individual handling (the
// closing quote, an escape, a control character, or an ill-formed/truncated
// sequence). ASCII is skipped 8 bytes at a time.
inline std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
// sequence). ASCII is skipped 16 bytes at a time with SSE2 and 8 bytes at a
// time otherwise; runs of non-ASCII text use the vector UTF-8 check where
// available.
JSON_HEDLEY_ALWAYS_INLINE std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
{
std::size_t pos = 0;
while (pos < n)
const unsigned char* const e = data + n;
const unsigned char* p = data;
while (p != e)
{
pos += find_string_special(data + pos, n - pos);
if (pos >= n || data[pos] < 0x80u)
#if JSON_SCAN_VECTOR
p = plain_run_end(p, e);
#else
p += find_string_special(p, static_cast<std::size_t>(e - p));
#endif
if (p == e || *p < 0x80u)
{
break; // end of buffer, or a quote/escape/control byte
}
#if JSON_SCAN_VECTOR_UTF8
#if JSON_SCAN_SSSE3_DISPATCH
if (JSON_HEDLEY_LIKELY(cpu_has_ssse3()))
#endif
{
return static_cast<std::size_t>(scan_string_vector(p, e) - data);
}
#endif
#if !JSON_SCAN_VECTOR_UTF8 || JSON_SCAN_SSSE3_DISPATCH
// a run of multi-byte sequences (e.g. CJK text) is validated sequence
// by sequence without searching for the next special byte in between
do
{
const std::size_t seq = validate_one_utf8(data + pos, n - pos);
const std::size_t seq = validate_one_utf8(p, static_cast<std::size_t>(e - p));
if (seq == 0)
{
return pos; // ill-formed or truncated: let the byte path diagnose it
return static_cast<std::size_t>(p - data); // ill-formed or truncated: let the byte path diagnose it
}
pos += seq;
p += seq;
}
while (pos < n && data[pos] >= 0x80u);
while (p != e && *p >= 0x80u);
#endif
}
return pos;
return static_cast<std::size_t>(p - data);
}
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
@@ -304,7 +406,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t
// next delimiter is validated in one shot by simdutf; on the rare failure the
// scalar helper recomputes the exact valid prefix so the byte path still
// produces the precise diagnostic. Without it, the pure scalar path is used.
inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept
JSON_HEDLEY_ALWAYS_INLINE std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept
{
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
const std::size_t run = find_string_delimiter(data, n);
+74 -139
View File
@@ -280,7 +280,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
}
/// @throw invalid_iterator.214 always
JSON_HEDLEY_NO_RETURN void throw_cannot_get_value() const
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_cannot_get_value() const
{
JSON_THROW(invalid_iterator::create(exception_id::iterator_value_unavailable, "cannot get value", m_object));
}
@@ -294,40 +294,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{
JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{
case value_t::object:
{
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return m_it.object_iterator->second;
}
case value_t::array:
{
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
return *m_it.array_iterator;
}
case value_t::null:
throw_cannot_get_value();
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
{
return *m_object;
}
throw_cannot_get_value();
}
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
return *m_it.array_iterator;
}
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return m_it.object_iterator->second;
}
// neither array nor object: a primitive value (null has no value to point to)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type != value_t::null && m_it.primitive_iterator.is_begin()))
{
return *m_object;
}
throw_cannot_get_value();
}
/*!
@@ -338,38 +323,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{
JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{
case value_t::object:
{
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return &(m_it.object_iterator->second);
}
case value_t::array:
{
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
return &*m_it.array_iterator;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
{
return m_object;
}
throw_cannot_get_value();
}
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
return &*m_it.array_iterator;
}
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return &(m_it.object_iterator->second);
}
// neither array nor object: a primitive value
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
{
return m_object;
}
throw_cannot_get_value();
}
/*!
@@ -391,35 +363,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{
JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{
case value_t::object:
{
std::advance(m_it.object_iterator, 1);
break;
}
case value_t::array:
{
std::advance(m_it.array_iterator, 1);
break;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
++m_it.primitive_iterator;
break;
}
++m_it.array_iterator;
return *this;
}
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
std::advance(m_it.object_iterator, 1);
return *this;
}
// neither array nor object: a primitive value
++m_it.primitive_iterator;
return *this;
}
@@ -442,35 +399,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{
JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{
case value_t::object:
{
std::advance(m_it.object_iterator, -1);
break;
}
case value_t::array:
{
std::advance(m_it.array_iterator, -1);
break;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
--m_it.primitive_iterator;
break;
}
--m_it.array_iterator;
return *this;
}
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
std::advance(m_it.object_iterator, -1);
return *this;
}
// neither array nor object: a primitive value
--m_it.primitive_iterator;
return *this;
}
@@ -484,7 +426,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
// if objects are not the same, the comparison is undefined
if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
{
JSON_THROW(invalid_iterator::create(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object));
throw_literal<invalid_iterator>(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object);
}
// value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493
@@ -493,25 +435,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
return true;
}
switch (m_object->m_data.m_type)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{
case value_t::object:
return (m_it.object_iterator == other.m_it.object_iterator);
case value_t::array:
return (m_it.array_iterator == other.m_it.array_iterator);
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
return (m_it.primitive_iterator == other.m_it.primitive_iterator);
return (m_it.array_iterator == other.m_it.array_iterator);
}
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
return (m_it.object_iterator == other.m_it.object_iterator);
}
// neither array nor object: a primitive value
return (m_it.primitive_iterator == other.m_it.primitive_iterator);
}
/*!
@@ -533,7 +468,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
// if objects are not the same, the comparison is undefined
if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
{
JSON_THROW(invalid_iterator::create(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object));
throw_literal<invalid_iterator>(exception_id::iterators_compare_different_values, "cannot compare iterators of different containers", m_object);
}
// value-initialized forward iterators can be compared, and must compare equal to other value-initialized iterators of the same type #4493
@@ -546,7 +481,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
switch (m_object->m_data.m_type)
{
case value_t::object:
JSON_THROW(invalid_iterator::create(exception_id::iterator_order_on_object, "cannot compare order of object iterators", m_object));
throw_literal<invalid_iterator>(exception_id::iterator_order_on_object, "cannot compare order of object iterators", m_object);
case value_t::array:
return (m_it.array_iterator < other.m_it.array_iterator);
@@ -602,7 +537,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
switch (m_object->m_data.m_type)
{
case value_t::object:
JSON_THROW(invalid_iterator::create(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object));
throw_literal<invalid_iterator>(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object);
case value_t::array:
{
@@ -681,7 +616,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
switch (m_object->m_data.m_type)
{
case value_t::object:
JSON_THROW(invalid_iterator::create(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object));
throw_literal<invalid_iterator>(exception_id::iterator_arithmetic_on_object, "cannot use offsets with object iterators", m_object);
case value_t::array:
return m_it.array_iterator - other.m_it.array_iterator;
@@ -710,7 +645,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
switch (m_object->m_data.m_type)
{
case value_t::object:
JSON_THROW(invalid_iterator::create(exception_id::iterator_subscript_on_object, "cannot use operator[] for object iterators", m_object));
throw_literal<invalid_iterator>(exception_id::iterator_subscript_on_object, "cannot use operator[] for object iterators", m_object);
case value_t::array:
return *std::next(m_it.array_iterator, n);
@@ -750,7 +685,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
return m_it.object_iterator->first;
}
JSON_THROW(invalid_iterator::create(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators", m_object));
throw_literal<invalid_iterator>(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators", m_object);
}
/*!
+347 -90
View File
@@ -16,14 +16,13 @@
#include <iosfwd> // ostream
#endif // JSON_NO_IO
#include <limits> // max
#include <map> // map
#include <numeric> // accumulate
#include <string> // string
#include <utility> // move
#include <vector> // vector
#include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp>
@@ -69,12 +68,21 @@ class json_pointer
/// @sa https://json.nlohmann.me/api/json_pointer/to_string/
string_t to_string() const
{
return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
string_t{},
[](const string_t& a, const string_t& b)
// one buffer: every token adds its escaped form (at least its length) and a slash
std::size_t length = 0;
for (const auto& reference_token : reference_tokens)
{
return detail::concat<string_t>(a, '/', detail::escape(b));
});
length += reference_token.size() + 1;
}
string_t result;
result.reserve(length);
for (const auto& reference_token : reference_tokens)
{
result.append("/", 1);
detail::escape_into(result, reference_token);
}
return result;
}
/// @brief return a string representation of the JSON pointer
@@ -129,21 +137,29 @@ class json_pointer
friend json_pointer operator/(const json_pointer& lhs,
const json_pointer& rhs)
{
return json_pointer(lhs) /= rhs;
json_pointer result;
result.reference_tokens.reserve(lhs.reference_tokens.size() + rhs.reference_tokens.size());
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end());
return result;
}
/// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
friend json_pointer operator/(const json_pointer& lhs, string_t token) // NOLINT(performance-unnecessary-value-param)
{
return json_pointer(lhs) /= std::move(token);
json_pointer result;
result.reference_tokens.reserve(lhs.reference_tokens.size() + 1);
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
result.reference_tokens.push_back(std::move(token));
return result;
}
/// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
friend json_pointer operator/(const json_pointer& lhs, std::size_t array_idx)
{
return json_pointer(lhs) /= array_idx;
return lhs / detail::to_string<string_t>(array_idx);
}
/// @brief returns the parent of this JSON pointer
@@ -155,8 +171,8 @@ class json_pointer
return *this;
}
json_pointer res = *this;
res.pop_back();
json_pointer res;
res.reference_tokens = std::vector<string_t>(reference_tokens.begin(), reference_tokens.end() - 1);
return res;
}
@@ -245,14 +261,14 @@ class json_pointer
private:
/// @throw out_of_range.405 always
JSON_HEDLEY_NO_RETURN static void throw_no_parent()
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_no_parent()
{
JSON_THROW(detail::out_of_range::create(detail::exception_id::patch_on_root, "JSON pointer has no parent", nullptr));
}
/// @throw out_of_range.404 always
template<typename BasicJsonContext>
JSON_HEDLEY_NO_RETURN static void throw_unresolved(const string_t& reference_token, BasicJsonContext context)
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE static void throw_unresolved(const string_t& reference_token, BasicJsonContext context)
{
static_cast<void>(reference_token); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(context);
@@ -305,6 +321,28 @@ class json_pointer
return array_index_status::not_a_number;
}
// fast path: up to 18 decimal digits cannot overflow unsigned long long or
// size_type (9 digits for a 32-bit size_type), so strtoull and errno are not needed
constexpr std::size_t max_fast_digits = sizeof(size_type) >= 8 ? 18 : 9;
if (!s.empty() && s.size() <= max_fast_digits)
{
size_type acc = 0;
for (std::size_t i = 0; i < s.size(); ++i)
{
const char c = s[i];
if (JSON_HEDLEY_UNLIKELY(c < '0' || c > '9'))
{
break; // not a plain number: let strtoull decide
}
acc = static_cast<size_type>(acc * 10 + static_cast<size_type>(c - '0'));
if (JSON_HEDLEY_UNLIKELY(i == s.size() - 1))
{
idx = acc;
return array_index_status::ok;
}
}
}
const char* p = s.data();
char* p_end = nullptr; // NOLINT(misc-const-correctness)
errno = 0; // strtoull doesn't reset errno
@@ -361,6 +399,83 @@ class json_pointer
return idx;
}
/*!
@brief compare with a JSON pointer string without splitting it into tokens
@throw parse_error.107 if the string is not empty or begins with '/'
@throw parse_error.108 if character '~' is not followed by '0' or '1'
*/
bool equals_string(const string_t& reference_string) const
{
if (reference_string.empty())
{
return reference_tokens.empty();
}
if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/'))
{
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr));
}
// the whole string is validated, whether or not it matches
const std::size_t size = reference_string.size();
for (std::size_t pos = reference_string.find_first_of('~');
pos != string_t::npos;
pos = reference_string.find_first_of('~', pos + 1))
{
if (JSON_HEDLEY_UNLIKELY(pos == size - 1 || (reference_string[pos + 1] != '0' && reference_string[pos + 1] != '1')))
{
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
}
}
// compare token by token, unescaping the string on the fly
std::size_t pos = 0; // the slash in front of the next token
for (const auto& reference_token : reference_tokens)
{
if (pos >= size)
{
return false;
}
std::size_t i = pos + 1;
std::size_t j = 0;
const std::size_t token_size = reference_token.size();
while (i < size && reference_string[i] != '/')
{
char c = reference_string[i++];
if (c == '~')
{
c = (reference_string[i++] == '0') ? '~' : '/';
}
if (j == token_size || reference_token[j] != c)
{
return false;
}
++j;
}
if (j != token_size)
{
return false;
}
pos = i;
}
return pos == size;
}
/// @brief object member access with a key that is only copied if it is inserted
template<typename BasicJsonType>
static BasicJsonType& object_member(BasicJsonType& object, const string_t& key)
{
auto& members = *object.m_data.m_value.object;
const auto it = members.find(key);
if (it != members.end())
{
return it->second;
}
return object.operator[](key);
}
JSON_PRIVATE_UNLESS_TESTED:
json_pointer top() const
{
@@ -384,84 +499,141 @@ class json_pointer
the result independent of the order in which the flattened object is
iterated, which is unspecified for some object types.
The prefixes form a tree and are numbered, so each of them is stored only
once (as a node) rather than as a copy of all of its reference tokens.
The prefixes form a tree and are numbered (number 0 is the empty prefix),
so each of them is stored only once (as a node) rather than as a copy of
all of its reference tokens. The tree does not own the reference tokens:
it refers to the tokens of the parsed keys, which must outlive it.
*/
struct prefix_tree
{
// children[id] maps a reference token to the number of the prefix
// extended by that token; number 0 is the empty prefix
std::vector<std::map<string_t, std::size_t>> children;
// open-addressing hash table from (parent, last reference token) to
// the number of the node; empty slots have token == nullptr
struct slot
{
std::size_t hash;
std::size_t parent;
const string_t* token;
std::size_t id;
slot() noexcept
: hash(0), parent(0), token(nullptr), id(0)
{}
slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept
: hash(h), parent(p), token(t), id(i)
{}
};
std::vector<slot> slots;
std::size_t used = 0;
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
{
std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL);
for (std::size_t i = 0; i < token.size(); ++i)
{
h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL);
}
return h ^ (h >> 29u);
}
void grow()
{
std::vector<slot> old(slots.size() * 2);
old.swap(slots);
const std::size_t mask = slots.size() - 1;
for (const auto& s : old)
{
if (s.token != nullptr)
{
std::size_t i = s.hash & mask;
while (slots[i].token != nullptr)
{
i = (i + 1) & mask;
}
slots[i] = s;
}
}
}
// parent[id] is the number of the prefix without its last token
std::vector<std::size_t> parent;
// is_array[id] is true iff some flattened key has the reference token
// 0 directly below the prefix with number id
std::vector<bool> is_array;
// start with the empty prefix only
prefix_tree()
: children(1)
: slots(16)
, parent(1, 0)
, is_array(1, false)
{}
// return the number of the prefix with number id extended by
// reference_token, adding it if it is new
std::size_t add_child(std::size_t id, string_t&& reference_token)
std::size_t add_child(std::size_t id, const string_t& reference_token)
{
const std::size_t hash = hash_of(id, reference_token);
const std::size_t mask = slots.size() - 1;
std::size_t i = hash & mask;
while (slots[i].token != nullptr)
{
if (slots[i].hash == hash && slots[i].parent == id && *slots[i].token == reference_token)
{
return slots[i].id;
}
i = (i + 1) & mask;
}
// new prefix
const std::size_t child = parent.size();
slots[i] = slot(hash, id, &reference_token, child);
if (reference_token == "0")
{
is_array[id] = true;
}
parent.push_back(id);
is_array.push_back(false);
// read the number before the emplace_back below, which may
// reallocate children and invalidate the iterator
const std::size_t next = children.size();
const auto inserted = children[id].emplace(std::move(reference_token), next);
const std::size_t child = inserted.first->second;
if (inserted.second)
// keep the load factor below 1/2
if (++used * 2 > slots.size())
{
children.emplace_back();
is_array.push_back(false);
grow();
}
return child;
}
// return the number of the prefix with number id extended by
// reference_token, which must have been added before
std::size_t find_child(std::size_t id, const string_t& reference_token) const
{
const auto it = children[id].find(reference_token);
JSON_ASSERT(it != children[id].end());
return it->second;
}
};
/*!
@brief create and return a reference to the pointed to value
Complexity: Linear in the number of reference tokens (times the logarithm
of the number of siblings for the prefix lookup).
@param[in] j the value to create the pointed to value in
@param[in] tokens the reference tokens of the pointer
@param[in] prefixes prefixes[i] is the number in @a tree of the prefix
consisting of the first i reference tokens
Complexity: Linear in the number of reference tokens.
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if array index is not a number
@throw type_error.313 if value cannot be unflattened
*/
template<typename BasicJsonType>
BasicJsonType& get_and_create(BasicJsonType& j, const prefix_tree& tree) const
static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens,
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
{
auto* result = &j;
// the number of the prefix consumed so far; used to look up whether
// the value to be created below is an array or an object
std::size_t id = 0;
// in case no reference tokens exist, return a reference to the JSON value
// j which will be overwritten by a primitive value
for (const auto& reference_token : reference_tokens)
for (std::size_t i = 0; i < tokens.size(); ++i)
{
const auto& reference_token = tokens[i];
switch (result->type())
{
case detail::value_t::null:
{
if (tree.is_array[id])
if (tree.is_array[prefixes[i]])
{
// some reference token below this position is 0, so the
// value is an array
@@ -478,7 +650,7 @@ class json_pointer
case detail::value_t::object:
{
// create an entry in the object
result = &result->operator[](reference_token);
result = &object_member(*result, reference_token);
break;
}
@@ -505,8 +677,6 @@ class json_pointer
default:
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j));
}
id = tree.find_child(id, reference_token);
}
return *result;
@@ -558,7 +728,7 @@ class json_pointer
case detail::value_t::object:
{
// use unchecked object access
ptr = &ptr->operator[](reference_token);
ptr = &object_member(*ptr, reference_token);
break;
}
@@ -608,8 +778,13 @@ class json_pointer
{
case detail::value_t::object:
{
// note: at performs range check
ptr = &ptr->at(reference_token);
const auto it = ptr->m_data.m_value.object->find(reference_token);
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
{
// at() throws out_of_range.403 with the usual message
ptr->at(reference_token);
}
ptr = &it->second;
break;
}
@@ -630,7 +805,7 @@ class json_pointer
JSON_THROW(detail::out_of_range::create(detail::exception_id::array_index_out_of_range, detail::concat(
"array index ", std::to_string(idx), " is out of range"), ptr));
}
ptr = &ptr->operator[](idx);
ptr = &(*ptr->m_data.m_value.array)[idx];
break;
}
@@ -678,7 +853,9 @@ class json_pointer
{
// use unchecked object access; the const operator[]
// asserts that the key exists
ptr = &ptr->operator[](reference_token);
const auto it = ptr->m_data.m_value.object->find(reference_token);
JSON_ASSERT(it != ptr->m_data.m_value.object->end());
ptr = &it->second;
break;
}
@@ -692,7 +869,9 @@ class json_pointer
// use unchecked array access; the const operator[]
// asserts that the index exists
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
const auto idx = array_index<BasicJsonType>(reference_token);
JSON_ASSERT(idx < ptr->m_data.m_value.array->size());
ptr = &(*ptr->m_data.m_value.array)[idx];
break;
}
@@ -733,12 +912,12 @@ class json_pointer
{
case detail::value_t::object:
{
const auto it = ptr->find(reference_token);
if (JSON_HEDLEY_UNLIKELY(it == ptr->end()))
const auto it = ptr->m_data.m_value.object->find(reference_token);
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
{
return nullptr;
}
ptr = &*it;
ptr = &it->second;
break;
}
@@ -775,7 +954,7 @@ class json_pointer
{
return nullptr;
}
ptr = &ptr->operator[](idx);
ptr = &(*ptr->m_data.m_value.array)[idx];
break;
}
@@ -808,13 +987,14 @@ class json_pointer
{
case detail::value_t::object:
{
if (!ptr->contains(reference_token))
const auto it = ptr->m_data.m_value.object->find(reference_token);
if (it == ptr->m_data.m_value.object->end())
{
// we did not find the key in the object
return false;
}
ptr = &ptr->operator[](reference_token);
ptr = &it->second;
break;
}
@@ -836,13 +1016,13 @@ class json_pointer
return false;
}
if (idx >= ptr->size())
if (idx >= ptr->m_data.m_value.array->size())
{
// index out of range
return false;
}
ptr = &ptr->operator[](idx);
ptr = &(*ptr->m_data.m_value.array)[idx];
break;
}
@@ -892,6 +1072,15 @@ class json_pointer
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr));
}
// every slash starts a reference token
const std::size_t size = reference_string.size();
std::size_t token_count = 0;
for (std::size_t i = 0; i < size; ++i)
{
token_count += (reference_string[i] == '/') ? 1 : 0;
}
result.reserve(token_count);
// extract the reference tokens:
// - slash: position of the last read slash (or end of string)
// - start: position after the previous slash
@@ -913,31 +1102,52 @@ class json_pointer
const auto count = (slash == string_t::npos ? reference_string.size() : slash) - start;
auto reference_token = string_t(reference_string.data() + start, count);
// check reference tokens are properly escaped
for (std::size_t pos = reference_token.find_first_of('~');
pos != string_t::npos;
pos = reference_token.find_first_of('~', pos + 1))
// check reference tokens are properly escaped; a token without
// a '~' (the common case) needs neither check nor unescaping
std::size_t pos = reference_token.find_first_of('~');
if (pos != string_t::npos)
{
JSON_ASSERT(reference_token[pos] == '~');
// ~ must be followed by 0 or 1
if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 ||
(reference_token[pos + 1] != '0' &&
reference_token[pos + 1] != '1')))
for (; pos != string_t::npos; pos = reference_token.find_first_of('~', pos + 1))
{
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
JSON_ASSERT(reference_token[pos] == '~');
// ~ must be followed by 0 or 1
if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 ||
(reference_token[pos + 1] != '0' &&
reference_token[pos + 1] != '1')))
{
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
}
}
detail::unescape(reference_token);
}
// finally, store the reference token
detail::unescape(reference_token);
result.push_back(reference_token);
result.push_back(std::move(reference_token));
}
return result;
}
private:
// Add the entry (key, v) to the object `obj`. Flattened paths are pairwise
// distinct, so objects that can append a key known to be absent
// (ordered_map) skip the lookup, which is linear and would make flatten
// quadratic; all other object types use operator[].
template<typename ObjectType, typename ValueType>
static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
-> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void())
{
obj.emplace_new_key(key, std::forward<ValueType>(v));
}
template<typename ObjectType, typename ValueType>
static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
{
obj[key] = std::forward<ValueType>(v);
}
/*!
@param[in] reference_string the reference string to the current value
@param[in] value the value to consider
@@ -982,7 +1192,8 @@ class json_pointer
// flatten `v`, whose path is `path`: primitives and empty containers
// are added to the result right away; other containers get a frame
const auto enter = [&stack, &path, &result](const BasicJsonType & v)
auto& object = *result.m_data.m_value.object;
const auto enter = [&stack, &path, &object](const BasicJsonType & v)
{
switch (v.type())
{
@@ -991,7 +1202,7 @@ class json_pointer
if (v.m_data.m_value.array->empty())
{
// flatten empty array as null
result[path] = nullptr;
add_flattened(object, path, nullptr, detail::priority_tag<1> {});
}
else
{
@@ -1005,7 +1216,7 @@ class json_pointer
if (v.m_data.m_value.object->empty())
{
// flatten empty object as null
result[path] = nullptr;
add_flattened(object, path, nullptr, detail::priority_tag<1> {});
}
else
{
@@ -1025,7 +1236,7 @@ class json_pointer
default:
{
// add a primitive value with its reference string
result[path] = v;
add_flattened(object, path, v, detail::priority_tag<1> {});
return;
}
}
@@ -1071,6 +1282,10 @@ class json_pointer
enter(it->second);
}
}
// the entries were added without going through basic_json, so the
// parent pointers (JSON_DIAGNOSTICS) have to be set now
result.set_parents();
}
/*!
@@ -1094,21 +1309,53 @@ class json_pointer
BasicJsonType result;
// parse every key once; the reference tokens are kept for the second
// pass (the vectors are never resized, so the tree may refer to them)
std::vector<std::vector<string_t>> keys;
keys.reserve(value.m_data.m_value.object->size());
for (const auto& element : *value.m_data.m_value.object)
{
json_pointer ptr(element.first);
keys.push_back(std::move(ptr.reference_tokens));
}
// collect the pointer prefixes that have a reference token 0 among
// their children; the values below them are arrays, all others are
// objects (see prefix_tree)
prefix_tree tree;
for (const auto& element : *value.m_data.m_value.object)
// the number of the prefix with i + 1 tokens of the previous key; keys
// are often sorted, so most of the prefix is the same as for the
// previous key and need not be looked up again
std::vector<std::size_t> path;
const std::vector<string_t>* previous = nullptr;
// the number of the complete prefix (i.e., the key) of each key
std::vector<std::size_t> leaves;
leaves.reserve(keys.size());
for (const auto& tokens : keys)
{
json_pointer ptr(element.first);
std::size_t id = 0;
for (auto& reference_token : ptr.reference_tokens)
std::size_t common = 0;
if (previous != nullptr)
{
id = tree.add_child(id, std::move(reference_token));
const std::size_t limit = (std::min)(tokens.size(), previous->size());
while (common < limit && tokens[common] == (*previous)[common])
{
++common;
}
}
path.resize(common);
std::size_t id = common == 0 ? 0 : path[common - 1];
for (std::size_t i = common; i < tokens.size(); ++i)
{
id = tree.add_child(id, tokens[i]);
path.push_back(id);
}
leaves.push_back(id);
previous = &tokens;
}
// iterate the JSON object values
std::vector<std::size_t> prefixes;
std::size_t key_index = 0;
for (const auto& element : *value.m_data.m_value.object)
{
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
@@ -1116,11 +1363,21 @@ class json_pointer
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second));
}
const auto& tokens = keys[key_index];
// number the prefixes of this key from its last one to the first
prefixes.resize(tokens.size());
for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i)
{
id = tree.parent[id];
prefixes[i - 1] = id;
}
++key_index;
// Assign the value to the reference pointed to by JSON pointer. Note
// that if the JSON pointer is "" (i.e., points to the whole value),
// function get_and_create returns a reference to the result itself.
// An assignment will then create a primitive value.
json_pointer(element.first).get_and_create(result, tree) = element.second;
get_and_create(result, tokens, prefixes, tree) = element.second;
}
return result;
@@ -1159,7 +1416,7 @@ class json_pointer
= delete;
#else
{
return *this == json_pointer(rhs);
return equals_string(rhs);
}
#endif
@@ -1243,7 +1500,7 @@ inline bool operator==(const json_pointer<RefStringTypeLhs>& lhs,
= delete;
#else
{
return lhs == json_pointer<RefStringTypeLhs>(rhs);
return lhs.equals_string(rhs);
}
#endif
@@ -1256,7 +1513,7 @@ inline bool operator==(const StringType& lhs,
= delete;
#else
{
return json_pointer<RefStringTypeRhs>(lhs) == rhs;
return rhs.equals_string(lhs);
}
#endif
+8
View File
@@ -895,3 +895,11 @@
#ifndef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 0
#endif
#ifndef JSON_DISABLE_STREAM_BULK_SCAN
#define JSON_DISABLE_STREAM_BULK_SCAN 0
#endif
#ifndef JSON_DISABLE_UNLOCKED_FILE_IO
#define JSON_DISABLE_UNLOCKED_FILE_IO 0
#endif
+10
View File
@@ -21,8 +21,18 @@
#undef JSON_NO_UNIQUE_ADDRESS
#undef JSON_DISABLE_ENUM_SERIALIZATION
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
#undef JSON_DISABLE_STREAM_BULK_SCAN
#undef JSON_DISABLE_UNLOCKED_FILE_IO
#undef JSON_UNLOCKED_FILE_IO
#undef JSON_DTOA_SSE2
#undef JSON_DTOA_NEON
#undef JSON_SCAN_NEON
#undef JSON_SCAN_SSE2
#undef JSON_SCAN_SSSE3
#undef JSON_SCAN_SSSE3_DISPATCH
#undef JSON_SCAN_SSSE3_TARGET
#undef JSON_SCAN_VECTOR
#undef JSON_SCAN_VECTOR_UTF8
#ifndef JSON_TEST_KEEP_MACROS
#undef JSON_CATCH
@@ -9,9 +9,11 @@
#pragma once
#include <limits> // numeric_limits
#include <map> // map, multimap
#include <string> // char_traits
#include <tuple> // tuple
#include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type
#include <unordered_map> // unordered_map, unordered_multimap
#include <utility> // declval
#include <vector> // vector
#if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L
@@ -683,6 +685,22 @@ struct is_compatible_integer_type
: is_compatible_integer_type_impl<RealIntegerType,
CompatibleNumberIntegerType> {};
// whether to_json() stores an integer of type CompatibleNumberIntegerType as
// number_integer
template<typename BasicJsonType, typename CompatibleNumberIntegerType, typename = void>
struct is_compatible_number_integer_type
: is_compatible_integer_type<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType> {};
// if number_unsigned_t is the same type as number_integer_t, unsigned integers
// are stored as number_integer as well (#2573)
template<typename BasicJsonType, typename CompatibleNumberIntegerType>
struct is_compatible_number_integer_type<BasicJsonType, CompatibleNumberIntegerType,
enable_if_t<conjunction<std::is_same<typename BasicJsonType::number_integer_t, typename BasicJsonType::number_unsigned_t>,
std::is_integral<CompatibleNumberIntegerType>,
std::is_unsigned<CompatibleNumberIntegerType>,
negation<std::is_same<bool, CompatibleNumberIntegerType>>>::value>>
: std::is_constructible<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType> {};
template<typename BasicJsonType, typename CompatibleType, typename = void>
struct is_compatible_type_impl: std::false_type {};
@@ -897,6 +915,31 @@ struct has_capacity : std::integral_constant<bool, is_detected<detect_capacity,
template <typename T>
struct is_ordered_map : has_capacity<T> {};
// whether T is a specialization of nlohmann::ordered_map, which only ever
// appends members, so they can only move when its capacity changes
template<typename T>
struct is_nlohmann_ordered_map : std::false_type {};
template<class Key, class T, class IgnoredLess, class Allocator>
struct is_nlohmann_ordered_map<ordered_map<Key, T, IgnoredLess, Allocator>> : std::true_type {};
// whether adding or erasing elements never moves the other elements of the
// container T; only node-based standard containers are known to guarantee this
template<typename T>
struct has_stable_elements : std::false_type {};
template<class Key, class T, class Compare, class Allocator>
struct has_stable_elements<std::map<Key, T, Compare, Allocator>> : std::true_type {};
template<class Key, class T, class Compare, class Allocator>
struct has_stable_elements<std::multimap<Key, T, Compare, Allocator>> : std::true_type {};
template<class Key, class T, class Hash, class KeyEqual, class Allocator>
struct has_stable_elements<std::unordered_map<Key, T, Hash, KeyEqual, Allocator>> : std::true_type {};
template<class Key, class T, class Hash, class KeyEqual, class Allocator>
struct has_stable_elements<std::unordered_multimap<Key, T, Hash, KeyEqual, Allocator>> : std::true_type {};
// to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324)
template < typename T, typename U, enable_if_t < !std::is_same<T, U>::value, int > = 0 >
T conditional_static_cast(U value)
@@ -1020,6 +1063,25 @@ constexpr bool value_in_range_of(T val)
template<bool Value>
using bool_constant = std::integral_constant<bool, Value>;
// Stands in for number_unsigned_t in overload sets if it is the same type as
// number_integer_t, so that the unsigned overloads do not collide with the
// integer ones. Never constructed. Such a basic_json stores every integer as
// number_integer (#2573).
struct same_as_number_integer_type {};
// NumberUnsignedType, or same_as_number_integer_type if it is the same type as
// NumberIntegerType; basic_json uses this one, as it cannot name its own member
// types through an incomplete basic_json (MSVC reports a recursive alias, and
// old GCC versions crash)
template<typename NumberIntegerType, typename NumberUnsignedType>
using number_unsigned_overload_type = typename std::conditional<std::is_same<NumberIntegerType, NumberUnsignedType>::value,
same_as_number_integer_type, NumberUnsignedType>::type;
// number_unsigned_t of BasicJsonType, or same_as_number_integer_type if it is
// the same type as number_integer_t
template<typename BasicJsonType>
using number_unsigned_overload_t = number_unsigned_overload_type<typename BasicJsonType::number_integer_t, typename BasicJsonType::number_unsigned_t>;
///////////////////////////////////////////////////////////////////////////////
// is_c_string
///////////////////////////////////////////////////////////////////////////////
@@ -221,7 +221,7 @@ class binary_writer
case value_t::number_unsigned:
{
write_cbor_head(0x00, j.m_data.m_value.number_unsigned);
write_cbor_head(0x00, conditional_static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned));
break;
}
@@ -1601,7 +1601,7 @@ class binary_writer
return calc_bson_integer_size(j.m_data.m_value.number_integer);
case value_t::number_unsigned:
return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned);
return calc_bson_unsigned_size(conditional_static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned));
case value_t::string:
return calc_bson_string_size(*j.m_data.m_value.string, j);
@@ -1643,7 +1643,7 @@ class binary_writer
return write_bson_integer(name, j.m_data.m_value.number_integer);
case value_t::number_unsigned:
return write_bson_unsigned(name, j.m_data.m_value.number_unsigned);
return write_bson_unsigned(name, conditional_static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned));
case value_t::string:
return write_bson_string(name, *j.m_data.m_value.string);
+178 -94
View File
@@ -49,6 +49,12 @@ class serializer
using number_float_t = typename BasicJsonType::number_float_t;
using number_integer_t = typename BasicJsonType::number_integer_t;
using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
/// type of the absolute value of an integer: number_unsigned_t, or its
/// unsigned counterpart if it is signed (it may be number_integer_t, #2573)
using number_magnitude_t = typename std::conditional <
std::is_integral<number_unsigned_t>::value && std::is_signed<number_unsigned_t>::value,
std::make_unsigned<number_unsigned_t>,
std::enable_if<true, number_unsigned_t >>::type::type;
using binary_char_t = typename BasicJsonType::binary_t::value_type;
public:
@@ -772,6 +778,33 @@ class serializer
break;
}
}
// A byte below 0x60 is a complete code point and needs no
// UTF-8 decoding. The scanner stops only at a control
// character, a quote, or a backslash in that range, so look up
// its escape sequence and write all 8 bytes of the table entry
// straight into the write buffer; only the length stored in the
// last byte of the entry is kept.
if (data[i] < 0x60)
{
// emit any bytes still pending in string_buffer first to
// preserve output order
if (bytes != 0)
{
put_buffer(string_buffer, bytes);
bytes = 0;
}
const auto& escape = ascii_escapes()[data[i]];
if (JSON_HEDLEY_UNLIKELY(write_buffer.size() - write_buffer_pos < escape.size()))
{
flush();
}
std::memcpy(write_buffer.data() + write_buffer_pos, escape.data(), escape.size());
write_buffer_pos += static_cast<std::size_t>(escape[7]);
bytes_after_last_accept = 0;
undumped_chars = 0;
continue;
}
}
const auto byte = static_cast<std::uint8_t>(s[i]);
@@ -780,92 +813,39 @@ class serializer
{
case UTF8_ACCEPT: // decode found a new code point
{
switch (codepoint)
// the code point is at least 0x60 (smaller ones are handled
// above) and is not printable ASCII (which the scanner
// copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII
// character
if (EnsureAscii)
{
case 0x08: // backspace
JSON_ASSERT(codepoint >= 0x7F);
if (error_handler == error_handler_t::keep)
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 'b';
break;
// 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;
}
case 0x09: // horizontal tab
if (codepoint <= 0xFFFF)
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 't';
break;
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
}
case 0x0A: // newline
else
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 'n';
break;
}
case 0x0C: // formfeed
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 'f';
break;
}
case 0x0D: // carriage return
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 'r';
break;
}
case 0x22: // quotation mark
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = '"';
break;
}
case 0x5C: // reverse solidus
{
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = '\\';
break;
}
default:
{
// escape control characters (0x00..0x1F) or, if
// EnsureAscii parameter is used, non-ASCII characters
if ((codepoint <= 0x1F) || (EnsureAscii && (codepoint >= 0x7F)))
{
if (EnsureAscii && error_handler == error_handler_t::keep)
{
// 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));
}
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
// been copied have in default case above)
string_buffer[bytes++] = s[i];
}
break;
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
// sequence have been copied in the default case below)
string_buffer[bytes++] = s[i];
}
// write buffer and reset index; there must be 13 bytes
// left, as this is the maximal number of bytes to be
@@ -1227,7 +1207,7 @@ class serializer
@param[in] x unsigned integer number to count its digits
@return number of decimal digits
*/
unsigned int count_digits(number_unsigned_t x) noexcept
unsigned int count_digits(number_magnitude_t x) noexcept
{
unsigned int n_digits = 1;
for (;;)
@@ -1253,6 +1233,121 @@ class serializer
}
}
/*!
@brief the JSON form of each character below 0x60
Entry c holds the escape sequence of character c - the short form for
\\b, \\t, \\n, \\f, \\r, \\" and \\\\, \\u00xx for the other control characters -
or c itself if it needs no escaping, padded with zeros to 7 bytes. The last
byte is the length of the sequence. The 8 bytes of an entry are copied at
once, so writing an escape needs neither a branch nor a byte-wise loop.
*/
static const std::array<std::array<char, 8>, 0x60>& ascii_escapes() noexcept
{
static const std::array<std::array<char, 8>, 0x60> escapes =
{
{
{{'\\', 'u', '0', '0', '0', '0', 0, 6}}, // 0x00
{{'\\', 'u', '0', '0', '0', '1', 0, 6}}, // 0x01
{{'\\', 'u', '0', '0', '0', '2', 0, 6}}, // 0x02
{{'\\', 'u', '0', '0', '0', '3', 0, 6}}, // 0x03
{{'\\', 'u', '0', '0', '0', '4', 0, 6}}, // 0x04
{{'\\', 'u', '0', '0', '0', '5', 0, 6}}, // 0x05
{{'\\', 'u', '0', '0', '0', '6', 0, 6}}, // 0x06
{{'\\', 'u', '0', '0', '0', '7', 0, 6}}, // 0x07
{{'\\', 'b', 0, 0, 0, 0, 0, 2}}, // 0x08
{{'\\', 't', 0, 0, 0, 0, 0, 2}}, // 0x09
{{'\\', 'n', 0, 0, 0, 0, 0, 2}}, // 0x0A
{{'\\', 'u', '0', '0', '0', 'b', 0, 6}}, // 0x0B
{{'\\', 'f', 0, 0, 0, 0, 0, 2}}, // 0x0C
{{'\\', 'r', 0, 0, 0, 0, 0, 2}}, // 0x0D
{{'\\', 'u', '0', '0', '0', 'e', 0, 6}}, // 0x0E
{{'\\', 'u', '0', '0', '0', 'f', 0, 6}}, // 0x0F
{{'\\', 'u', '0', '0', '1', '0', 0, 6}}, // 0x10
{{'\\', 'u', '0', '0', '1', '1', 0, 6}}, // 0x11
{{'\\', 'u', '0', '0', '1', '2', 0, 6}}, // 0x12
{{'\\', 'u', '0', '0', '1', '3', 0, 6}}, // 0x13
{{'\\', 'u', '0', '0', '1', '4', 0, 6}}, // 0x14
{{'\\', 'u', '0', '0', '1', '5', 0, 6}}, // 0x15
{{'\\', 'u', '0', '0', '1', '6', 0, 6}}, // 0x16
{{'\\', 'u', '0', '0', '1', '7', 0, 6}}, // 0x17
{{'\\', 'u', '0', '0', '1', '8', 0, 6}}, // 0x18
{{'\\', 'u', '0', '0', '1', '9', 0, 6}}, // 0x19
{{'\\', 'u', '0', '0', '1', 'a', 0, 6}}, // 0x1A
{{'\\', 'u', '0', '0', '1', 'b', 0, 6}}, // 0x1B
{{'\\', 'u', '0', '0', '1', 'c', 0, 6}}, // 0x1C
{{'\\', 'u', '0', '0', '1', 'd', 0, 6}}, // 0x1D
{{'\\', 'u', '0', '0', '1', 'e', 0, 6}}, // 0x1E
{{'\\', 'u', '0', '0', '1', 'f', 0, 6}}, // 0x1F
{{' ', 0, 0, 0, 0, 0, 0, 1}}, // 0x20
{{'!', 0, 0, 0, 0, 0, 0, 1}}, // 0x21
{{'\\', '"', 0, 0, 0, 0, 0, 2}}, // 0x22
{{'#', 0, 0, 0, 0, 0, 0, 1}}, // 0x23
{{'$', 0, 0, 0, 0, 0, 0, 1}}, // 0x24
{{'%', 0, 0, 0, 0, 0, 0, 1}}, // 0x25
{{'&', 0, 0, 0, 0, 0, 0, 1}}, // 0x26
{{'\'', 0, 0, 0, 0, 0, 0, 1}}, // 0x27
{{'(', 0, 0, 0, 0, 0, 0, 1}}, // 0x28
{{')', 0, 0, 0, 0, 0, 0, 1}}, // 0x29
{{'*', 0, 0, 0, 0, 0, 0, 1}}, // 0x2A
{{'+', 0, 0, 0, 0, 0, 0, 1}}, // 0x2B
{{',', 0, 0, 0, 0, 0, 0, 1}}, // 0x2C
{{'-', 0, 0, 0, 0, 0, 0, 1}}, // 0x2D
{{'.', 0, 0, 0, 0, 0, 0, 1}}, // 0x2E
{{'/', 0, 0, 0, 0, 0, 0, 1}}, // 0x2F
{{'0', 0, 0, 0, 0, 0, 0, 1}}, // 0x30
{{'1', 0, 0, 0, 0, 0, 0, 1}}, // 0x31
{{'2', 0, 0, 0, 0, 0, 0, 1}}, // 0x32
{{'3', 0, 0, 0, 0, 0, 0, 1}}, // 0x33
{{'4', 0, 0, 0, 0, 0, 0, 1}}, // 0x34
{{'5', 0, 0, 0, 0, 0, 0, 1}}, // 0x35
{{'6', 0, 0, 0, 0, 0, 0, 1}}, // 0x36
{{'7', 0, 0, 0, 0, 0, 0, 1}}, // 0x37
{{'8', 0, 0, 0, 0, 0, 0, 1}}, // 0x38
{{'9', 0, 0, 0, 0, 0, 0, 1}}, // 0x39
{{':', 0, 0, 0, 0, 0, 0, 1}}, // 0x3A
{{';', 0, 0, 0, 0, 0, 0, 1}}, // 0x3B
{{'<', 0, 0, 0, 0, 0, 0, 1}}, // 0x3C
{{'=', 0, 0, 0, 0, 0, 0, 1}}, // 0x3D
{{'>', 0, 0, 0, 0, 0, 0, 1}}, // 0x3E
{{'?', 0, 0, 0, 0, 0, 0, 1}}, // 0x3F
{{'@', 0, 0, 0, 0, 0, 0, 1}}, // 0x40
{{'A', 0, 0, 0, 0, 0, 0, 1}}, // 0x41
{{'B', 0, 0, 0, 0, 0, 0, 1}}, // 0x42
{{'C', 0, 0, 0, 0, 0, 0, 1}}, // 0x43
{{'D', 0, 0, 0, 0, 0, 0, 1}}, // 0x44
{{'E', 0, 0, 0, 0, 0, 0, 1}}, // 0x45
{{'F', 0, 0, 0, 0, 0, 0, 1}}, // 0x46
{{'G', 0, 0, 0, 0, 0, 0, 1}}, // 0x47
{{'H', 0, 0, 0, 0, 0, 0, 1}}, // 0x48
{{'I', 0, 0, 0, 0, 0, 0, 1}}, // 0x49
{{'J', 0, 0, 0, 0, 0, 0, 1}}, // 0x4A
{{'K', 0, 0, 0, 0, 0, 0, 1}}, // 0x4B
{{'L', 0, 0, 0, 0, 0, 0, 1}}, // 0x4C
{{'M', 0, 0, 0, 0, 0, 0, 1}}, // 0x4D
{{'N', 0, 0, 0, 0, 0, 0, 1}}, // 0x4E
{{'O', 0, 0, 0, 0, 0, 0, 1}}, // 0x4F
{{'P', 0, 0, 0, 0, 0, 0, 1}}, // 0x50
{{'Q', 0, 0, 0, 0, 0, 0, 1}}, // 0x51
{{'R', 0, 0, 0, 0, 0, 0, 1}}, // 0x52
{{'S', 0, 0, 0, 0, 0, 0, 1}}, // 0x53
{{'T', 0, 0, 0, 0, 0, 0, 1}}, // 0x54
{{'U', 0, 0, 0, 0, 0, 0, 1}}, // 0x55
{{'V', 0, 0, 0, 0, 0, 0, 1}}, // 0x56
{{'W', 0, 0, 0, 0, 0, 0, 1}}, // 0x57
{{'X', 0, 0, 0, 0, 0, 0, 1}}, // 0x58
{{'Y', 0, 0, 0, 0, 0, 0, 1}}, // 0x59
{{'Z', 0, 0, 0, 0, 0, 0, 1}}, // 0x5A
{{'[', 0, 0, 0, 0, 0, 0, 1}}, // 0x5B
{{'\\', '\\', 0, 0, 0, 0, 0, 2}}, // 0x5C
{{']', 0, 0, 0, 0, 0, 0, 1}}, // 0x5D
{{'^', 0, 0, 0, 0, 0, 0, 1}}, // 0x5E
{{'_', 0, 0, 0, 0, 0, 0, 1}}, // 0x5F
}
};
return escapes;
}
/*!
* @brief write a lowercase "\\uXXXX" escape sequence into @a string_buffer
*
@@ -1410,7 +1505,7 @@ class serializer
}
auto* buffer_ptr = write_buffer.data() + write_buffer_pos;
number_unsigned_t abs_value;
number_magnitude_t abs_value;
// one byte for the minus sign
unsigned int n_chars = 0;
@@ -1423,7 +1518,7 @@ class serializer
}
else
{
abs_value = static_cast<number_unsigned_t>(x);
abs_value = static_cast<number_magnitude_t>(x);
}
// up to 16 digits: eight at a time (as the digits of floats), written
@@ -1591,17 +1686,6 @@ class serializer
}
}
/*
* Overload to make the compiler happy while it is instantiating
* dump_integer for number_unsigned_t.
* Must never be called.
*/
number_unsigned_t remove_sign(number_unsigned_t x)
{
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
return x; // LCOV_EXCL_LINE
}
/*
* Helper function for dump_integer
*
@@ -1611,10 +1695,10 @@ class serializer
* absolute values of INT_MIN and INT_MAX are usually not the same. See
* #1708 for details.
*/
number_unsigned_t remove_sign(number_integer_t x) noexcept
number_magnitude_t remove_sign(number_integer_t x) noexcept
{
JSON_ASSERT(x < 0);
return static_cast<number_unsigned_t>(-(x + 1)) + 1;
return static_cast<number_magnitude_t>(-(x + 1)) + 1;
}
private:
+32 -24
View File
@@ -9,6 +9,7 @@
#pragma once
#include <cstddef> // size_t
#include <utility> // move
#include <nlohmann/detail/abi_macros.hpp>
@@ -24,39 +25,46 @@ namespace detail
* Note the order of escaping "~" to "~0" and "/" to "~1" is important.
*
* The string is rebuilt in a single pass, appending whole runs between the
* characters that need escaping. Scanning with find_first_of() keeps the
* common case -- nothing to escape -- as fast as a single search, while
* repeated replace() calls would move the tail of the string once per
* escaped character.
* characters that need escaping, while repeated replace() calls would move
* the tail of the string once per escaped character. @ref escape_into appends
* the escaped string to an existing one.
*/
template<typename StringType>
inline void escape_into(StringType& out, const StringType& s)
{
const std::size_t size = s.size();
std::size_t run = 0;
for (std::size_t i = 0; i < size; ++i)
{
const char c = s[i];
if (c == '~' || c == '/')
{
out.append(s.data() + run, i - run);
out.append(c == '~' ? "~0" : "~1", 2);
run = i + 1;
}
}
out.append(s.data() + run, size - run);
}
template<typename StringType>
inline StringType escape(const StringType& s)
{
auto next_special = [&s](std::size_t from)
// a single scan for both special characters; nothing to escape is the common case
const std::size_t size = s.size();
std::size_t i = 0;
while (i < size && s[i] != '~' && s[i] != '/')
{
const auto tilde = s.find_first_of('~', from);
const auto slash = s.find_first_of('/', from);
return tilde < slash ? tilde : slash; // npos is the largest value
};
auto pos = next_special(0);
if (pos == StringType::npos)
++i;
}
if (i == size)
{
return s;
}
StringType result;
result.reserve(s.size() + 2);
std::size_t run = 0;
while (pos != StringType::npos)
{
result.append(s.data() + run, pos - run);
result.append(s[pos] == '~' ? "~0" : "~1", 2);
run = pos + 1;
pos = next_special(run);
}
result.append(s.data() + run, s.size() - run);
result.reserve(size + 2);
escape_into(result, s);
return result;
}
@@ -101,7 +109,7 @@ inline void unescape(StringType& s)
pos = s.find_first_of('~', run);
}
result.append(s.data() + run, s.size() - run);
s = result;
s = std::move(result);
}
} // namespace detail
+28
View File
@@ -117,6 +117,34 @@ inline bool operator<(const value_t lhs, const value_t rhs) noexcept
}
#endif
/*!
@brief whether a value of the given type owns heap memory
Strings, binaries, objects, and arrays are stored as pointers to allocated
values; all other types are stored directly in the union. Every enumerator is
listed so that -Wswitch-enum flags this function when a new type is added.
*/
inline bool owns_memory(const value_t t) noexcept
{
switch (t)
{
case value_t::object:
case value_t::array:
case value_t::string:
case value_t::binary:
return true;
case value_t::null:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::discarded:
default:
return false;
}
}
/*!
@brief compare an integer with a floating point number without precision loss
+683 -312
View File
File diff suppressed because it is too large. Load diff
+21 -1
View File
@@ -90,6 +90,15 @@ private:
return self.end();
}
/// @brief throw the exception of a failed at() lookup
/// @note kept out of line so that at_impl() stays small enough to inline
/// @throw std::out_of_range always
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE
static void throw_key_not_found()
{
JSON_THROW(std::out_of_range("key not found"));
}
/// @brief shared implementation of the const and non-const at() overloads
/// @throw std::out_of_range if @a key is not found
template<typename Self, typename KeyType>
@@ -98,7 +107,7 @@ private:
const auto it = find_impl(self, key);
if (it == self.end())
{
JSON_THROW(std::out_of_range("key not found"));
throw_key_not_found();
}
return it->second;
}
@@ -143,6 +152,17 @@ public:
return {std::prev(this->end()), true};
}
/// @brief add an element at the end without checking whether the key exists
/// @note The caller must guarantee that @a key is not yet contained, otherwise
/// the container holds duplicate keys. This avoids the linear search of
/// emplace() and operator[] when many keys known to be new are added.
template<class V, detail::enable_if_t<
detail::is_constructible<T, V>::value, int> = 0>
void emplace_new_key(const key_type& key, V && t)
{
append(key, std::forward<V>(t));
}
T& operator[](const key_type& key)
{
return emplace(key, T{}).first->second;
+2903 -813
View File
File diff suppressed because it is too large. Load diff
+50
View File
@@ -285,6 +285,10 @@ if (JSON_TestSimdutf)
endif()
endif()
# test-file_input checks from a second thread that parsing from a FILE* releases the file's lock
find_package(Threads REQUIRED)
json_test_set_test_options("test-file_input;test-file_input_unlocked_io_disabled" LINK_LIBRARIES Threads::Threads)
# *DO* use json_test_set_test_options() above this line
json_test_should_build_32bit_test(json_32bit_test json_32bit_test_only "${JSON_32bitTest}")
@@ -369,6 +373,52 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# the lexer again with the portable string scanning instead of SSE2/NEON, and on
# x86-64 with SSSE3 compiled in (no check of the CPU)
json_test_set_test_options(test-class_lexer_no_simd
COMPILE_DEFINITIONS JSON_NO_SIMD
)
json_test_add_test_for(src/unit-class_lexer.cpp
NAME test-class_lexer_no_simd
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC)
json_test_set_test_options(test-class_lexer_ssse3
COMPILE_OPTIONS -mssse3
)
json_test_add_test_for(src/unit-class_lexer.cpp
NAME test-class_lexer_ssse3
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
endif()
# test reading from a std::istream again without scanning the get area in bulk
json_test_set_test_options(test-stream-bulk-scan_disabled
COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1
)
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
NAME test-stream-bulk-scan_disabled
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test reading from a std::istream again with JSON_PRECISE_STREAM_POSITION
json_test_set_test_options(test-stream-bulk-scan_precise
COMPILE_DEFINITIONS JSON_PRECISE_STREAM_POSITION=1
)
json_test_add_test_for(src/unit-stream-bulk-scan.cpp
NAME test-stream-bulk-scan_precise
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
# test reading from a FILE* again with the locking stdio functions
json_test_set_test_options(test-file_input_unlocked_io_disabled
COMPILE_DEFINITIONS JSON_DISABLE_UNLOCKED_FILE_IO=1
)
json_test_add_test_for(src/unit-file_input.cpp
NAME test-file_input_unlocked_io_disabled
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
)
endif()
# *DO NOT* use json_test_set_test_options() below this line
+87
View File
@@ -8,8 +8,13 @@
#include <benchmark/benchmark.h>
#include <nlohmann/json.hpp>
#include <array>
#include <cstdio>
#include <deque>
#include <fstream>
#include <numeric>
#include <set>
#include <sstream>
#include <vector>
#include <test_data.hpp>
@@ -48,6 +53,65 @@ BENCHMARK_CAPTURE(ParseFile, signed_ints, TEST_DATA_DIRECTORY "/regression
BENCHMARK_CAPTURE(ParseFile, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
BENCHMARK_CAPTURE(ParseFile, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
//////////////////////////////////////////////////////////////////////////////
// parse JSON from a FILE*
//////////////////////////////////////////////////////////////////////////////
static void ParseFILE(benchmark::State& state, const char* filename)
{
while (state.KeepRunning())
{
state.PauseTiming();
std::FILE* f = std::fopen(filename, "rb");
auto* j = new json();
state.ResumeTiming();
*j = json::parse(f);
state.PauseTiming();
std::fclose(f);
delete j;
state.ResumeTiming();
}
std::ifstream file(filename, std::ios::binary | std::ios::ate);
state.SetBytesProcessed(state.iterations() * file.tellg());
}
BENCHMARK_CAPTURE(ParseFILE, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
BENCHMARK_CAPTURE(ParseFILE, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
BENCHMARK_CAPTURE(ParseFILE, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
//////////////////////////////////////////////////////////////////////////////
// read many small values from one stream (NDJSON)
//////////////////////////////////////////////////////////////////////////////
static void ParseStreamValues(benchmark::State& state, bool numbers)
{
std::string input;
for (int i = 0; i < 10000; ++i)
{
input += numbers ? std::to_string(i * 7919) + " "
: R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","active":true,"score":)" + std::to_string(i * 0.25) + "}\n";
}
for (auto _ : state)
{
std::istringstream is(input);
json j;
std::size_t count = 0;
while (!(is >> std::ws).eof())
{
is >> j;
++count;
}
benchmark::DoNotOptimize(count);
}
state.SetBytesProcessed(static_cast<int64_t>(state.iterations() * input.size()));
}
BENCHMARK_CAPTURE(ParseStreamValues, ndjson, false);
BENCHMARK_CAPTURE(ParseStreamValues, numbers, true);
//////////////////////////////////////////////////////////////////////////////
// parse JSON from string
//////////////////////////////////////////////////////////////////////////////
@@ -621,4 +685,27 @@ static void FromCborChunkedString(benchmark::State& state, const std::size_t chu
BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000);
// conversion of a JSON array into standard containers
template<typename Container>
static void GetArray(benchmark::State& state)
{
json j = json::array();
for (int i = 0; i < 1000; ++i)
{
j.push_back(i);
}
for (auto _ : state)
{
auto c = j.get<Container>();
benchmark::DoNotOptimize(c);
}
state.SetItemsProcessed(state.iterations() * 1000);
}
BENCHMARK_TEMPLATE(GetArray, std::vector<int>);
BENCHMARK_TEMPLATE(GetArray, std::deque<int>);
BENCHMARK_TEMPLATE(GetArray, std::set<int>);
BENCHMARK_TEMPLATE(GetArray, std::array<int, 1000>);
BENCHMARK_MAIN();
+10 -1
View File
@@ -9,7 +9,16 @@ Each fuzzer driver (`tests/src/fuzzer-parse_*.cpp`) parses its input twice: once
once with exceptions. Both calls must agree. Where parsing with exceptions fails, the call without exceptions must
return a discarded value (or throw the same kind of non-parse error), and it must never throw a `parse_error`. Where
parsing succeeds, both calls must return the same value. The drivers then serialize the value, parse the result back,
and check that nothing was lost. The drivers check all of this with `assert`, so they refuse to build with `NDEBUG`.
and check that nothing was lost.
Finally, each driver reads the same input through the other input adapters: a `std::istringstream`, a non-contiguous
iterator range (`std::list<std::uint8_t>`), and, on POSIX systems, a `FILE*` from `fmemopen`. The pointer range takes the
fast paths for contiguous input (bulk string scanning, multi-byte loads, lazy position tracking), while the other
adapters read byte by byte. Every adapter must give the same value, or the same exception with the same id, message,
and byte position. The JSON driver checks this for `parse` and `accept`, each with and without `ignore_comments`. The
shared code is `check_input_adapters()` in `tests/src/fuzzer_common.hpp`.
The drivers check all of this with `assert`, so they refuse to build with `NDEBUG`.
## Corpus creation
+21
View File
@@ -47,12 +47,30 @@ dumps is stable under exactly the same values that break operator==.
The unit tests run the same checks on a fixed corpus (see the "BJData round-trip
invariants" test case), so keep both in sync.
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
error (id, message, byte position) as the pointer range, which takes the bulk
fast paths (see check_input_adapters()).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include "fuzzer_common.hpp"
namespace
{
// json::from_bjdata for check_input_adapters()
struct read_bjdata
{
template<typename... Args>
json operator()(Args&& ... args) const
{
return json::from_bjdata(std::forward<Args>(args)...);
}
};
} // namespace
// value-stable comparison for the round-trip checks below; see the note
// above on why this compares dump()s rather than the json values directly
static bool is_value_stable(const json& lhs, const json& rhs)
@@ -126,6 +144,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
check_input_adapters(data, size, read_bjdata());
// return 0 - non-zero return values are reserved for future use
return 0;
}
+13 -21
View File
@@ -17,44 +17,33 @@ array data, it performs the following steps:
- j2 = from_bon8(vec)
- assert(to_bon8(j2) == vec)
It also checks that reading the data from a stream, which reads strings byte by
byte, gives the same value or error as reading it from contiguous memory, which
copies strings in bulk.
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
error (id, message, byte position) as the pointer range, which takes the bulk
fast paths (see check_input_adapters()).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include <sstream>
#include "fuzzer_common.hpp"
namespace
{
// the serialization of the value read from @a input, or the error message
template<typename InputType>
std::string read_bon8(InputType&& input)
// json::from_bon8 for check_input_adapters()
struct read_bon8
{
try
template<typename... Args>
json operator()(Args&& ... args) const
{
const auto vec = json::to_bon8(json::from_bon8(std::forward<InputType>(input)));
return {vec.begin(), vec.end()};
return json::from_bon8(std::forward<Args>(args)...);
}
catch (const json::exception& e)
{
return e.what();
}
}
};
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
// contiguous and stream input must be read alike
{
std::istringstream stream(std::string(reinterpret_cast<const char*>(data), size));
assert(read_bon8(std::vector<uint8_t>(data, data + size)) == read_bon8(stream));
}
std::vector<uint8_t> const vec1(data, data + size);
// step 0: parse input without exceptions
@@ -106,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
check_input_adapters(data, size, read_bon8());
// return 0 - non-zero return values are reserved for future use
return 0;
}
+21
View File
@@ -17,12 +17,30 @@ array data, it performs the following steps:
- j2 = from_bson(vec)
- assert(to_bson(j2) == vec)
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
error (id, message, byte position) as the pointer range, which takes the bulk
fast paths (see check_input_adapters()).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include "fuzzer_common.hpp"
namespace
{
// json::from_bson for check_input_adapters()
struct read_bson
{
template<typename... Args>
json operator()(Args&& ... args) const
{
return json::from_bson(std::forward<Args>(args)...);
}
};
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
@@ -77,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
check_input_adapters(data, size, read_bson());
// return 0 - non-zero return values are reserved for future use
return 0;
}
+21
View File
@@ -17,12 +17,30 @@ array data, it performs the following steps:
- j2 = from_cbor(vec)
- assert(to_cbor(j2) == vec)
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
error (id, message, byte position) as the pointer range, which takes the bulk
fast paths (see check_input_adapters()).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include "fuzzer_common.hpp"
namespace
{
// json::from_cbor for check_input_adapters()
struct read_cbor
{
template<typename... Args>
json operator()(Args&& ... args) const
{
return json::from_cbor(std::forward<Args>(args)...);
}
};
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
@@ -77,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
check_input_adapters(data, size, read_cbor());
// return 0 - non-zero return values are reserved for future use
return 0;
}
+42
View File
@@ -17,6 +17,12 @@ array data, it performs the following steps:
- j2 = parse(s1)
- s2 = serialize(j2)
- assert(s1 == s2)
- for ignore_comments = false and true: assert(parse(data) and accept(data)
give the same result for a stream, a non-contiguous iterator range, and a
FILE* as for the pointer range)
The pointer range takes the bulk fast paths, the other input adapters read byte
by byte; see check_input_adapters().
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
@@ -24,6 +30,32 @@ drivers.
#include "fuzzer_common.hpp"
namespace
{
// json::parse and json::accept for check_input_adapters()
struct parse_json
{
bool ignore_comments;
template<typename... Args>
json operator()(Args&& ... args) const
{
return json::parse(std::forward<Args>(args)..., nullptr, true, ignore_comments);
}
};
struct accept_json
{
bool ignore_comments;
template<typename... Args>
bool operator()(Args&& ... args) const
{
return json::accept(std::forward<Args>(args)..., ignore_comments);
}
};
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
@@ -76,6 +108,16 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
for (const bool ignore_comments :
{
false, true
})
{
check_input_adapters(data, size, parse_json{ignore_comments});
check_input_adapters(data, size, accept_json{ignore_comments});
}
// return 0 - non-zero return values are reserved for future use
return 0;
}
+21
View File
@@ -17,12 +17,30 @@ array data, it performs the following steps:
- j2 = from_msgpack(vec)
- assert(to_msgpack(j2) == vec)
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
error (id, message, byte position) as the pointer range, which takes the bulk
fast paths (see check_input_adapters()).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include "fuzzer_common.hpp"
namespace
{
// json::from_msgpack for check_input_adapters()
struct read_msgpack
{
template<typename... Args>
json operator()(Args&& ... args) const
{
return json::from_msgpack(std::forward<Args>(args)...);
}
};
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
@@ -77,6 +95,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
check_input_adapters(data, size, read_msgpack());
// return 0 - non-zero return values are reserved for future use
return 0;
}
+21
View File
@@ -26,12 +26,30 @@ array data, it performs the following steps:
The unit tests run the same checks on a fixed corpus (see the "UBJSON round-trip
invariants" test case), so keep both in sync.
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
error (id, message, byte position) as the pointer range, which takes the bulk
fast paths (see check_input_adapters()).
The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer
drivers.
*/
#include "fuzzer_common.hpp"
namespace
{
// json::from_ubjson for check_input_adapters()
struct read_ubjson
{
template<typename... Args>
json operator()(Args&& ... args) const
{
return json::from_ubjson(std::forward<Args>(args)...);
}
};
} // namespace
// see http://llvm.org/docs/LibFuzzer.html
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
@@ -96,6 +114,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
assert(parsed || noexcept_threw || j_noexcept.is_discarded());
}
// step 3: the other input adapters must read the data alike
check_input_adapters(data, size, read_ubjson());
// return 0 - non-zero return values are reserved for future use
return 0;
}
+94
View File
@@ -11,6 +11,14 @@
#pragma once
#include <cassert> // assert
#include <cstddef> // size_t
#include <cstdint> // uint8_t
#include <cstdio> // FILE, fmemopen, fclose
#include <list> // list
#include <sstream> // istringstream
#include <string> // string
#include <utility> // forward
#include <vector> // vector
#include <nlohmann/json.hpp>
using nlohmann::json; // NOLINT(google-global-names-in-headers): shared by all fuzzer drivers
@@ -49,3 +57,89 @@ json parse_without_exceptions(Parse parse, bool& threw)
}
return {};
}
// the outcome of reading an input with exceptions: the serialization of the
// value, or the id, message, and (for parse errors) byte of the exception
struct read_outcome
{
std::string value{};
int id = 0;
std::string what{};
std::size_t byte = 0;
bool operator==(const read_outcome& rhs) const
{
return value == rhs.value && id == rhs.id && what == rhs.what && byte == rhs.byte;
}
};
inline std::string outcome_value(const json& j)
{
return j.dump(-1, ' ', false, json::error_handler_t::keep);
}
inline std::string outcome_value(bool b)
{
return b ? "true" : "false";
}
template<typename Read, typename... Args>
read_outcome read_with(const Read& parse, Args&& ... args)
{
read_outcome result;
try
{
result.value = outcome_value(parse(std::forward<Args>(args)...));
}
catch (const json::parse_error& e)
{
result.id = e.id;
result.what = e.what();
result.byte = e.byte;
}
catch (const json::exception& e)
{
result.id = e.id;
result.what = e.what();
}
return result;
}
// The contiguous input (a pointer range) takes the bulk fast paths; the other
// input adapters read byte by byte. Reads @a data through each of them and
// checks that all give the same value, or the same exception with the same
// id, message, and byte position.
//
// @a parse is called with either an iterator range or a single input (stream
// or FILE*) and returns the parsed value (or the result of accept()).
template<typename Read>
void check_input_adapters(const std::uint8_t* data, std::size_t size, const Read& parse)
{
const read_outcome expected = read_with(parse, data, data + size);
// stream adapter
{
std::istringstream stream(std::string(reinterpret_cast<const char*>(data), size));
assert(read_with(parse, stream) == expected);
}
// iterator adapter on a non-contiguous range
{
const std::list<std::uint8_t> list(data, data + size);
assert(read_with(parse, list.begin(), list.end()) == expected);
}
#if defined(__unix__) || defined(__APPLE__)
// FILE* adapter; fmemopen may refuse an empty buffer
{
std::vector<std::uint8_t> buffer(data, data + size);
std::FILE* file = buffer.empty() ? nullptr : fmemopen(buffer.data(), buffer.size(), "rb");
if (file != nullptr)
{
const read_outcome actual = read_with(parse, file);
std::fclose(file);
assert(actual == expected);
}
}
#endif
}
+1
View File
@@ -9,6 +9,7 @@
#include "doctest_compatibility.h"
#include <algorithm>
#include <numeric>
#include <nlohmann/json.hpp>
using nlohmann::json;
+173
View File
@@ -15,9 +15,12 @@ using nlohmann::json;
#include <array> // array
#include <cstdint> // uint32_t, uint64_t
#include <cstdio> // snprintf
#include <cmath> // isfinite
#include <cstdlib> // strtod
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <initializer_list> // initializer_list
#include <list> // list
#include <map> // map
#include <random> // mt19937
#include <sstream> // stringstream
@@ -332,6 +335,109 @@ TEST_CASE("lexer number fast path")
}
}
SECTION("digits accumulated while scanning")
{
// The fast path accumulates the digits while it validates them, eight
// at a time where possible, and converts tokens with at most 19
// digits (floats) or 18 digits (integers) from that value. Every digit
// count from 1 to 25, in the integer and in the fraction part, with
// and without sign and exponent, must give the byte path's result. A
// std::list has no contiguous storage, so parsing it takes the byte
// path.
// type and value, or the error (values beyond the range of double
// are rejected with out_of_range.406 on both paths)
const auto outcome = [](const std::string & doc, bool byte_path) -> std::string
{
JSON_TRY
{
const std::list<char> chars(doc.begin(), doc.end());
const json j = byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc);
return std::string(j[0].type_name()) + '|' + j.dump();
}
JSON_CATCH (const json::exception & e)
{
return e.what();
}
};
// the concatenation of @a parts
const auto join = [](std::initializer_list<std::string> parts)
{
std::string result;
for (const auto& part : parts)
{
result += part;
}
return result;
};
std::vector<std::string> numbers;
for (std::size_t digits = 1; digits <= 25; ++digits)
{
std::string run;
for (std::size_t i = 0; i < digits; ++i)
{
run += static_cast<char>('1' + static_cast<char>((i * 7) % 9));
}
const std::string nines(digits, '9');
for (const char* sign_text :
{"", "-"
})
{
const std::string sign = sign_text;
numbers.push_back(join({sign, run}));
numbers.push_back(join({sign, nines}));
numbers.push_back(join({sign, "1", std::string(digits - 1, '0')}));
numbers.push_back(join({sign, "0.", run}));
numbers.push_back(join({sign, "0.", std::string(digits, '0'), "1"}));
numbers.push_back(join({sign, run, ".", run}));
numbers.push_back(join({sign, run, "e-", std::to_string(digits)}));
numbers.push_back(join({sign, "1.", run, "E+", std::to_string(digits * 13)}));
}
}
// exponents beyond every range, and zeros
numbers.insert(numbers.end(), {"1e400", "1e-400", "-1e99999999999999999999", "1e-99999999999999999999",
"0e99999999999999999999", "0.0e-5", "-0.000", "123456789e-0000000000000000000000330"
});
for (const auto& n : numbers)
{
CAPTURE(n)
const std::string doc = "[" + n + "]";
const std::string a = outcome(doc, false);
CHECK(a == outcome(doc, true));
const double expected = std::strtod(n.c_str(), nullptr);
if (a.compare(0, 6, "number") == 0 && std::isfinite(expected) && n.find_first_of(".eE") != std::string::npos)
{
CHECK(json::parse(doc)[0].get<double>() == expected);
}
}
}
SECTION("integers that do not fit a narrower type become floats")
{
// up to 18 digits, the fast path stores the accumulated value without
// an overflow check; a narrower number_integer_t/number_unsigned_t
// must still see the value rejected and converted as a float
using json32 = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int32_t, std::uint32_t, float>;
for (const char* n :
{"4294967295", "4294967296", "2147483647", "-2147483648", "-2147483649", "99999999999", "-99999999999"
})
{
CAPTURE(n)
const std::string doc = std::string("[") + n + "]";
const std::list<char> chars(doc.begin(), doc.end());
const json32 a = json32::parse(doc);
const json32 b = json32::parse(chars.begin(), chars.end());
CHECK(a[0].type() == b[0].type());
CHECK(a == b);
}
CHECK(json32::parse("[4294967295]")[0].is_number_unsigned());
CHECK(json32::parse("[4294967296]")[0].is_number_float());
CHECK(json32::parse("[-2147483648]")[0].is_number_integer());
CHECK(json32::parse("[-2147483649]")[0].is_number_float());
}
SECTION("token type classification")
{
CHECK((scan_string("0") == json::lexer::token_type::value_unsigned));
@@ -610,6 +716,73 @@ TEST_CASE("lexer string fast path")
CAPTURE(mismatches)
CHECK(mismatches.empty());
}
SECTION("special bytes at every offset of the vector blocks")
{
// With SSE2 or NEON, runs are scanned 16 bytes at a time (after one
// SWAR word, or four with NEON), and runs of non-ASCII text are
// checked 16 bytes at a time as well. Place every kind of byte that
// ends a run at each offset across several blocks, after ASCII and
// after CJK text, so sequences also straddle the block boundaries.
// The reference is a std::list, which has no contiguous storage and
// is therefore read one character at a time.
const auto byte_path = [](const std::string & doc) -> std::string
{
const std::list<char> chars(doc.begin(), doc.end());
JSON_TRY
{
return json::parse(chars.begin(), chars.end()).dump();
}
JSON_CATCH (const json::exception & e)
{
return e.what();
}
};
const std::vector<std::string> specials =
{
"\"", "\\", "\\u00e9", bytes({0x01}), bytes({0x1F}), bytes({0x7F}),
bytes({0xC3, 0xA9}), bytes({0xE4, 0xB8, 0xAD}), bytes({0xF0, 0x9F, 0x98, 0x80}),
bytes({0xFF}), bytes({0xC3}), bytes({0xE4, 0xB8}), bytes({0xED, 0xA0, 0x80}), bytes({0xC0, 0x80})
};
const std::string cjk = bytes({0xE4, 0xB8, 0xAD}); // U+4E2D
std::vector<std::string> mismatches;
for (std::size_t offset = 0; offset <= 80; ++offset)
{
std::string text;
while (text.size() < offset)
{
text += cjk;
}
for (const auto& prefix :
{
std::string(offset, 'a'), text
})
{
for (const auto& special : specials)
{
// the closing quote right behind, or in a later block
for (const int tail :
{
0, 1, 3, 20
})
{
std::string doc = "[\"";
doc += prefix;
doc += special;
doc.append(static_cast<std::size_t>(tail), 'b');
doc += "\"]";
if (outcome(doc, false) != byte_path(doc))
{
mismatches.push_back(doc);
}
}
}
}
}
CAPTURE(mismatches)
CHECK(mismatches.empty());
}
#endif
// json::accept() never throws, so the ranges stay covered without exceptions
+75
View File
@@ -30,7 +30,9 @@ using nlohmann::json;
#include <valarray>
#include <algorithm>
#include <cstdio>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <list>
#include <sstream>
#include <string>
@@ -2450,6 +2452,79 @@ std::string parse_error_message_range(IteratorType first, IteratorType last)
}
} // namespace
TEST_CASE("text of float tokens")
{
// For contiguous input, the lexer copies the text of a float token into
// its buffer only when a SAX handler asks for it (the library's own
// handlers do not). A user handler must still see the exact text, for
// every input adapter and in every position of a document.
const std::string doc = R"([1.5, -0.0, 0.1, 1e5, -2.5E-4, 12345678901234567890.5, 0.12345678901234567890123,
{"a": 3.25, "b": [6.02e23, 1]}, "x", true, 7e-1])";
const std::vector<std::string> expected_floats =
{
"number_float(1.5)", "number_float(-0.0)", "number_float(0.1)", "number_float(1e5)", "number_float(-2.5E-4)",
"number_float(12345678901234567890.5)", "number_float(0.12345678901234567890123)", "number_float(3.25)",
"number_float(6.02e23)", "number_float(7e-1)"
};
const auto floats = [](const std::vector<std::string>& events)
{
std::vector<std::string> result;
std::copy_if(events.begin(), events.end(), std::back_inserter(result), [](const std::string & e)
{
return e.compare(0, 13, "number_float(") == 0;
});
return result;
};
SECTION("contiguous input")
{
SaxEventLogger l;
CHECK(json::sax_parse(doc, &l));
CHECK(floats(l.events) == expected_floats);
}
SECTION("byte input")
{
const std::vector<std::uint8_t> bytes(doc.begin(), doc.end());
SaxEventLogger l;
CHECK(json::sax_parse(bytes, &l));
CHECK(floats(l.events) == expected_floats);
}
SECTION("stream input")
{
std::istringstream is(doc);
SaxEventLogger l;
CHECK(json::sax_parse(is, &l));
CHECK(floats(l.events) == expected_floats);
}
SECTION("non-contiguous iterators")
{
const std::list<char> chars(doc.begin(), doc.end());
SaxEventLogger l;
CHECK(json::sax_parse(chars.begin(), chars.end(), &l));
CHECK(floats(l.events) == expected_floats);
}
SECTION("the library's own handlers do not need the text")
{
using lexer_adapter = nlohmann::detail::iterator_input_adapter<const char*>;
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_dom_parser<json, lexer_adapter>>::value);
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_dom_callback_parser<json, lexer_adapter>>::value);
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_acceptor<json>>::value);
CHECK_FALSE(nlohmann::detail::sax_ignores_number_text<SaxEventLogger>::value);
// and the values are the same as when the text is read
const json j = json::parse(doc);
CHECK(j[0] == 1.5);
CHECK(j[5].get<double>() == 12345678901234567890.5);
CHECK(j[7]["b"][0] == 6.02e23);
CHECK(json::accept(doc));
}
}
TEST_CASE("last-read diagnostics are identical across input adapters")
{
// The lexer reconstructs the "last read" token lazily for seekable adapters
+155
View File
@@ -19,6 +19,7 @@
#include <cctype>
#include <cstdint>
#include <limits>
#include <map>
#include <string>
#include <utility>
@@ -1149,3 +1150,157 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend
}
}
#endif
TEST_CASE("comparison of scalars (inline fast path)")
{
const json::number_integer_t neg = -1;
const json::number_integer_t pos = 5;
const json::number_unsigned_t big = (std::numeric_limits<json::number_unsigned_t>::max)();
SECTION("same type")
{
CHECK(json(pos) == json(pos));
CHECK(json(pos) != json(neg));
CHECK(json(neg) < json(pos));
CHECK_FALSE(json(pos) < json(neg));
CHECK_FALSE(json(pos) < json(pos));
CHECK(json(pos) <= json(pos));
CHECK(json(1u) == json(1u));
CHECK(json(1u) < json(2u));
CHECK_FALSE(json(2u) < json(1u));
CHECK(json(1.5) == json(1.5));
CHECK(json(1.5) < json(2.5));
CHECK_FALSE(json(2.5) < json(1.5));
CHECK(json(-0.0) == json(0.0));
}
SECTION("mixed number types")
{
// negative signed vs unsigned must not wrap around
CHECK(json(neg) < json(1u));
CHECK(json(1u) > json(neg));
CHECK(json(neg) != json(big));
CHECK(json(neg) < json(big));
CHECK_FALSE(json(big) < json(neg));
CHECK(json(pos) == json(5u));
CHECK(json(5u) == json(pos));
CHECK(json(pos) < json(6u));
CHECK(json(pos) == json(5.0));
CHECK(json(5.0) == json(pos));
CHECK(json(5u) == json(5.0));
CHECK(json(neg) < json(0.5));
CHECK(json(0.5) < json(1u));
}
SECTION("boolean and null")
{
CHECK(json(true) == json(true));
CHECK(json(true) != json(false));
CHECK(json(false) < json(true));
CHECK_FALSE(json(true) < json(false));
CHECK_FALSE(json(true) < json(true));
CHECK(json(nullptr) == json(nullptr));
CHECK_FALSE(json(nullptr) < json(nullptr));
CHECK(json(nullptr) <= json(nullptr));
CHECK(json(nullptr) < json(false));
CHECK(json(nullptr) != json(false));
CHECK(json(nullptr) != json(0));
}
SECTION("string vs number")
{
CHECK(json("5") != json(5));
CHECK(json(5) != json("5"));
CHECK(json(5) < json("5")); // number < string by type order
CHECK_FALSE(json("5") < json(5));
CHECK(json(1.5) < json(""));
CHECK(json("a") == json("a"));
CHECK(json("a") < json("b"));
}
SECTION("NaN")
{
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
const json one = 1.0;
CHECK_FALSE(nan == nan);
CHECK(nan != nan);
CHECK_FALSE(nan < nan);
CHECK_FALSE(nan <= nan);
CHECK_FALSE(nan >= nan);
CHECK_FALSE(nan > nan);
CHECK_FALSE(nan == one);
CHECK_FALSE(one == nan);
CHECK_FALSE(nan < one);
CHECK_FALSE(one < nan);
CHECK_FALSE(nan <= one);
CHECK_FALSE(one <= nan);
CHECK_FALSE(nan >= one);
CHECK_FALSE(one >= nan);
// NaN against other number types
CHECK_FALSE(nan == json(1));
CHECK_FALSE(json(1) == nan);
CHECK_FALSE(nan < json(1u));
CHECK_FALSE(json(1u) < nan);
#if JSON_HAS_THREE_WAY_COMPARISON
CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD*
CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD*
CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD*
CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD*
CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD*
CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD*
CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD*
CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD*
CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD*
#endif
}
SECTION("discarded")
{
const json d(json::value_t::discarded);
CHECK_FALSE(d == d);
CHECK_FALSE(d < d);
CHECK_FALSE(d == json(1));
CHECK_FALSE(json(1) == d);
CHECK_FALSE(d < json(1));
CHECK_FALSE(json(1) < d);
CHECK_FALSE(d == json(nullptr));
CHECK_FALSE(json(nullptr) == d);
#if JSON_HAS_THREE_WAY_COMPARISON
CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD*
CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD*
CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD*
#endif
}
SECTION("scalar overloads")
{
CHECK(json(5) == 5);
CHECK(5 == json(5));
CHECK(json(5) < 6);
CHECK(4 < json(5));
CHECK(json(1.5) == 1.5);
CHECK(json(true) == true);
CHECK(json(5) != 6);
CHECK(json(1u) == 1.0);
}
SECTION("structured values still compare")
{
CHECK(json({1, 2}) == json({1, 2}));
CHECK(json({1, 2}) < json({1, 3}));
CHECK(json({{"a", 1}}) == json({{"a", 1}}));
CHECK(json::binary({1, 2}) == json::binary({1, 2}));
CHECK(json(1) < json::array());
CHECK(json::object() < json::array());
}
}
+193
View File
@@ -8,6 +8,9 @@
#include "doctest_compatibility.h"
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
using nlohmann::json;
@@ -184,3 +187,193 @@ TEST_CASE("other constructors and destructor")
}
}
}
TEST_CASE("copy constructor and destructor across value types")
{
SECTION("copy of a scalar is independent of the source")
{
const json src_null = nullptr;
const json src_bool = true;
const json src_int = -7;
const json src_uint = 7u;
const json src_float = 2.5;
json c_null(src_null);
json c_bool(src_bool);
json c_int(src_int);
json c_uint(src_uint);
json c_float(src_float);
CHECK(c_null.is_null());
CHECK(c_bool == true);
CHECK(c_int == -7);
CHECK(c_uint == 7u);
CHECK(c_float == 2.5);
c_int = 8;
c_float = 3.5;
CHECK(src_int == -7);
CHECK(src_float == 2.5);
}
SECTION("copy of an owning value, then mutate the copy")
{
const json src_string = "hello";
const json src_array = {1, 2, 3};
const json src_object = {{"a", 1}, {"b", {{"c", 2}}}};
const json src_binary = json::binary({1, 2, 3});
json c_string(src_string);
json c_array(src_array);
json c_object(src_object);
json c_binary(src_binary);
CHECK(c_string == src_string);
CHECK(c_array == src_array);
CHECK(c_object == src_object);
CHECK(c_binary == src_binary);
c_string.get_ref<json::string_t&>() += " world";
c_array.push_back(4);
c_object["b"]["c"] = 3;
c_object["d"] = 4;
c_binary.get_binary().push_back(4);
CHECK(src_string == "hello");
CHECK(src_array.size() == 3);
CHECK(src_object["b"]["c"] == 2);
CHECK_FALSE(src_object.contains("d"));
CHECK(src_binary.get_binary().size() == 3);
CHECK(c_string == "hello world");
CHECK(c_array.size() == 4);
CHECK(c_object["b"]["c"] == 3);
CHECK(c_binary.get_binary().size() == 4);
}
SECTION("copy keeps parent pointers valid (JSON_DIAGNOSTICS)")
{
const json src = {{"a", {1, 2}}, {"b", "x"}};
json c(src);
c["a"].push_back(3);
CHECK(c["a"].size() == 3);
CHECK(src["a"].size() == 2);
}
SECTION("vector of mixed types: build, copy, and destroy")
{
{
std::vector<json> v;
for (int i = 0; i < 70; ++i)
{
switch (i % 9)
{
case 0:
v.emplace_back(nullptr);
break;
case 1:
v.emplace_back(i % 2 == 0);
break;
case 2:
v.emplace_back(-i);
break;
case 3:
v.emplace_back(static_cast<json::number_unsigned_t>(i));
break;
case 4:
v.emplace_back(i * 0.5);
break;
case 5:
v.emplace_back(std::string(static_cast<std::size_t>(i), 'x'));
break;
case 6:
v.emplace_back(json::array({i, "s", nullptr}));
break;
case 7:
v.emplace_back(json::object({{"k", i}}));
break;
default:
v.emplace_back(json::binary({1, 2, 3}));
break;
}
}
REQUIRE(v.size() == 70);
std::vector<json> copy(v);
CHECK(copy == v);
// mutating the copy leaves the original alone
copy[5] = "changed";
copy[6].push_back(1);
CHECK(copy != v);
CHECK(v[5] == std::string(5, 'x'));
CHECK(v[6].size() == 3);
v.erase(v.begin(), v.begin() + 10);
CHECK(v.size() == 60);
}
// destroying the discarded and default-constructed values
std::vector<json> w(4);
w.emplace_back(json::value_t::discarded);
w.emplace_back(json::value_t::object);
w.emplace_back(json::value_t::array);
w.emplace_back(json::value_t::string);
w.emplace_back(json::value_t::binary);
CHECK(w.size() == 9);
}
}
TEST_CASE_TEMPLATE("destructor of nested containers", Json, nlohmann::json, nlohmann::ordered_json)
{
SECTION("empty containers")
{
{
Json j = Json::array();
}
{
Json j = Json::object();
}
{
Json j = Json::array({Json::array(), Json::object(), Json::array({Json::array()})});
}
}
SECTION("leaves mixed with nested containers")
{
Json j = Json::object();
j["leaf"] = 1;
j["str"] = "text";
j["empty_a"] = Json::array();
j["empty_o"] = Json::object();
j["arr"] = Json::array({1, "a", Json::array({1, 2}), Json::object({{"k", Json::array({Json::object()})}}), nullptr});
j["obj"] = Json::object({{"x", Json::object({{"y", Json::array({1, Json::object({{"z", 1}})})}})}, {"w", 2}});
Json copy = j;
CHECK(copy == j);
j = Json::array({std::move(copy), j, 3, Json::array()});
}
SECTION("deep nesting")
{
for (int kind = 0; kind < 3; ++kind)
{
Json j = 1;
for (int i = 0; i < 5000; ++i)
{
switch (kind)
{
case 0:
j = Json::array({std::move(j)});
break;
case 1:
j = Json::object({{"k", std::move(j)}});
break;
default:
// siblings next to the nested child
j = (i % 2 == 0) ? Json::array({1, Json::array(), std::move(j)}) : Json::object({{"a", 1}, {"b", Json::object()}, {"c", std::move(j)}});
break;
}
}
}
}
}
+237
View File
@@ -13,9 +13,11 @@
#endif
#define JSON_DIAGNOSTICS 1
#define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
#include <map>
#include <unordered_map>
@@ -631,3 +633,238 @@ TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch()
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
}
}
namespace
{
// check that every child's parent pointer points to the containing value
template<typename BasicJsonType>
bool parents_consistent(const BasicJsonType& j)
{
bool ok = true;
if (j.is_array() || j.is_object())
{
for (const auto& child : j)
{
ok = ok && child.m_parent == &j && parents_consistent(child);
}
}
return ok;
}
} // namespace
TEST_CASE_TEMPLATE("Parent pointers stay consistent", J, json, ordered_json)
{
SECTION("parse, copy, move, assign, swap")
{
J j = J::parse(R"({"a":[1,{"b":[2,3]},[4]],"c":{"d":{"e":[]}}})");
CHECK(parents_consistent(j));
J copy(j);
CHECK(parents_consistent(copy));
J moved(std::move(copy));
CHECK(parents_consistent(moved));
J assigned;
assigned = j;
CHECK(parents_consistent(assigned));
J other = {1, 2, {{"k", J::array({1, 2})}}};
other.swap(assigned);
CHECK(parents_consistent(other));
CHECK(parents_consistent(assigned));
}
SECTION("push_back growth")
{
J arr = J::array();
for (int i = 0; i < 500; ++i)
{
arr.push_back(J::array({i}));
}
CHECK(parents_consistent(arr));
J obj = J::object();
for (int i = 0; i < 500; ++i)
{
obj.push_back({std::to_string(i), J::array({i})});
}
CHECK(parents_consistent(obj));
}
SECTION("emplace_back and emplace growth")
{
J arr = J::array();
J obj = J::object();
for (int i = 0; i < 500; ++i)
{
arr.emplace_back(J::array({i}));
obj.emplace(std::to_string(i), J::array({i}));
}
CHECK(parents_consistent(arr));
CHECK(parents_consistent(obj));
}
SECTION("operator[] growth and at()")
{
J obj = J::object();
for (int i = 0; i < 500; ++i)
{
obj[std::to_string(i)] = J::array({i});
CHECK(obj[std::to_string(i / 2)].m_parent == &obj);
}
CHECK(parents_consistent(obj));
J arr = J::array();
for (int i = 0; i < 500; ++i)
{
arr[static_cast<size_t>(i)] = J::object({{"x", i}});
}
for (size_t i = 0; i < 500; ++i)
{
CHECK(arr.at(i).m_parent == &arr);
}
CHECK(obj.at("3").m_parent == &obj);
CHECK(parents_consistent(arr));
CHECK(parents_consistent(obj));
}
}
TEST_CASE_TEMPLATE("update() and merge_patch() keep parent pointers and diagnostic paths", J, json, ordered_json)
{
// enough keys to make the vector behind ordered_json reallocate several times
const auto make_object = [](int first, int last)
{
J o = J::object();
for (int i = first; i < last; ++i)
{
o[std::to_string(i)] = J::object({{"x", J::array({i})}});
}
return o;
};
// const access, so operator[] cannot repair the parent pointers
const auto check_path = [](const J & j, const std::string & key)
{
const std::string expected = "[json.exception.type_error.304] (/" + key + "/x) cannot use at() with array";
CHECK_THROWS_WITH_AS(j[key]["x"].at("y"), expected.c_str(), json::type_error);
};
SECTION("update(const_reference)")
{
J j = make_object(0, 200);
j.update(make_object(100, 400));
CHECK(j.size() == 400);
CHECK(parents_consistent(j));
check_path(j, "0");
check_path(j, "150");
check_path(j, "399");
}
SECTION("update(const_reference, merge_objects)")
{
J j = make_object(0, 200);
j["50"]["y"] = 1;
J other = make_object(100, 400);
other["50"] = J::object({{"z", J::array({1})}});
j.update(other, true);
CHECK(j.size() == 400);
CHECK(j["50"].size() == 3);
CHECK(parents_consistent(j));
check_path(j, "0");
check_path(j, "399");
}
SECTION("update(first, last)")
{
J j = make_object(0, 200);
J other = make_object(100, 400);
j.update(other.begin(), other.end());
CHECK(j.size() == 400);
CHECK(parents_consistent(j));
check_path(j, "0");
check_path(j, "399");
J k = make_object(0, 200);
k.update(other.begin(), other.end(), true);
CHECK(k.size() == 400);
CHECK(parents_consistent(k));
check_path(k, "399");
}
SECTION("merge_patch()")
{
J j = make_object(0, 200);
J patch = make_object(100, 400);
patch["0"] = nullptr;
j.merge_patch(patch);
CHECK(j.size() == 399);
CHECK_FALSE(j.contains("0"));
CHECK(parents_consistent(j));
check_path(j, "1");
check_path(j, "399");
}
}
namespace
{
// an object type that, like a sorted flat map, moves its members when a key is
// added even if its capacity does not change; it has a capacity() member, but
// is neither node-based nor nlohmann::ordered_map, so the library must not
// trust its capacity to tell whether the members moved
template<class Key, class T, class IgnoredLess = std::less<Key>,
class Allocator = std::allocator<std::pair<const Key, T>>>
struct relocating_map : nlohmann::ordered_map<Key, T, IgnoredLess, Allocator>
{
using base_type = nlohmann::ordered_map<Key, T, IgnoredLess, Allocator>;
using typename base_type::iterator;
using typename base_type::Container;
using base_type::base_type;
template<typename... Args>
std::pair<iterator, bool> emplace(Args&& ... args)
{
auto result = base_type::emplace(std::forward<Args>(args)...);
const auto index = result.first - this->begin();
if (result.second)
{
// move every member to a new buffer of the same capacity
Container relocated;
relocated.reserve(this->capacity());
for (auto& member : *this)
{
relocated.emplace_back(member.first, std::move(member.second));
}
this->Container::swap(relocated);
}
return {this->begin() + index, result.second};
}
};
using relocating_json = nlohmann::basic_json<relocating_map>;
} // namespace
TEST_CASE("Parent pointers stay consistent for object types not known to keep their members in place")
{
relocating_json obj = relocating_json::object();
for (int i = 0; i < 100; ++i)
{
obj[std::to_string(i)] = relocating_json::array({i});
obj.emplace(std::to_string(i + 1000), relocating_json::array({i}));
}
CHECK(parents_consistent(obj));
obj.erase("0");
CHECK(parents_consistent(obj));
relocating_json other = relocating_json::object();
for (int i = 0; i < 100; ++i)
{
other[std::to_string(i + 2000)] = relocating_json::array({i});
}
obj.update(other);
CHECK(parents_consistent(obj));
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&);
}
+236
View File
@@ -0,0 +1,236 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <array>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <memory>
#include <random>
#include <string>
#include <thread>
#include <vector>
// Parsing from a FILE* locks the file once and reads it with the unlocked
// stdio functions (see JSON_DISABLE_UNLOCKED_FILE_IO). These tests pin that
// this is not observable: the results are the same, the file is unlocked
// again when parsing ends - also with an exception - and the same thread can
// keep using the file while it is locked, e.g., from a SAX callback.
namespace
{
// a file name of its own for every run: the test runs for several C++
// standards and configurations at the same time, in the same directory
std::string temp_file_name()
{
std::random_device rd;
return "unit-file_input-" + std::to_string(rd()) + ".tmp";
}
using file_ptr = std::unique_ptr<std::FILE, int (*)(std::FILE*)>;
// write @a content to the file @a name and open it for reading
file_ptr open_with(const char* name, const std::string& content)
{
{
std::ofstream file(name, std::ios::binary);
file << content;
}
return {std::fopen(name, "rb"), &std::fclose};
}
// The stdio lock is recursive, so whether parsing released it can only be
// seen from another thread: ftrylockfile() fails there while this thread
// holds the lock. ftrylockfile() is POSIX; elsewhere, the check is skipped.
#if defined(__GLIBC__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
void check_unlocked(std::FILE* f)
{
int result = -1;
std::thread other([f, &result]
{
result = ftrylockfile(f);
if (result == 0)
{
funlockfile(f);
}
});
other.join();
CHECK(result == 0);
}
#else
void check_unlocked(std::FILE* /*f*/) {}
#endif
// SAX consumer that also reads the position of the file it is parsing, which
// takes the file's lock again on the same thread
class position_reading_sax : public nlohmann::json_sax<json>
{
public:
explicit position_reading_sax(std::FILE* f) : m_file(f) {}
position_reading_sax(const position_reading_sax&) = delete;
position_reading_sax(position_reading_sax&&) = delete;
position_reading_sax& operator=(const position_reading_sax&) = delete;
position_reading_sax& operator=(position_reading_sax&&) = delete;
~position_reading_sax() override = default;
bool null() override
{
return record();
}
bool boolean(bool /*val*/) override
{
return record();
}
bool number_integer(number_integer_t /*val*/) override
{
return record();
}
bool number_unsigned(number_unsigned_t /*val*/) override
{
return record();
}
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
{
return record();
}
bool string(string_t& /*val*/) override
{
return record();
}
bool binary(binary_t& /*val*/) override
{
return record();
}
bool start_object(std::size_t /*elements*/) override
{
return record();
}
bool key(string_t& /*val*/) override
{
return record();
}
bool end_object() override
{
return record();
}
bool start_array(std::size_t /*elements*/) override
{
return record();
}
bool end_array() override
{
return record();
}
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const nlohmann::detail::exception& /*ex*/) override
{
return false;
}
const std::vector<long>& positions() const
{
return m_positions;
}
private:
bool record()
{
m_positions.push_back(std::ftell(m_file));
return true;
}
std::FILE* m_file;
std::vector<long> m_positions{}; // NOLINT(readability-redundant-member-init)
};
} // namespace
TEST_CASE("parsing from a FILE*")
{
const std::string temp_name = temp_file_name();
const char* const name = temp_name.c_str();
const std::string input = "{\"a\": [1, -2.5, \"three\", true, null], \"b\": {\"c\": \"caf\xc3\xa9\"}}";
SECTION("values are the same as when parsing from a string")
{
const file_ptr f = open_with(name, input);
REQUIRE(f != nullptr);
CHECK(json::parse(f.get()) == json::parse(input));
// the whole input was consumed
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) == nullptr);
check_unlocked(f.get());
}
SECTION("the file is unlocked after a parse error")
{
const file_ptr f = open_with(name, "[1, 2, x]");
REQUIRE(f != nullptr);
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(f.get()),
"[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - invalid literal; last read: '2, x'",
json::parse_error&);
check_unlocked(f.get());
// the file is still usable: the rest of the input is there
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
CHECK(std::string(rest.data()) == "]");
}
SECTION("the file can be parsed again and read with stdio afterwards")
{
const file_ptr f = open_with(name, "[1] [2] tail");
REQUIRE(f != nullptr);
// sax_parse with strict == false stops after the first value; an array
// ends with its closing bracket, so the space after it is not read
json j;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax1(j);
CHECK(json::sax_parse(f.get(), &sax1, json::input_format_t::json, false));
CHECK(j == json({1}));
check_unlocked(f.get());
json k;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax2(k);
CHECK(json::sax_parse(f.get(), &sax2, json::input_format_t::json, false));
CHECK(k == json({2}));
check_unlocked(f.get());
std::array<char, 8> rest{};
CHECK(std::fgets(rest.data(), static_cast<int>(rest.size()), f.get()) != nullptr);
CHECK(std::string(rest.data()) == " tail");
}
SECTION("the same thread can use the file while it is parsed")
{
const file_ptr f = open_with(name, "[10, 20, 30]");
REQUIRE(f != nullptr);
position_reading_sax sax(f.get());
CHECK(json::sax_parse(f.get(), &sax));
// start_array, three numbers, end_array
CHECK(sax.positions().size() == 5);
CHECK(sax.positions().front() == 1);
check_unlocked(f.get());
}
SECTION("binary formats read from a FILE*")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
const file_ptr f = open_with(name, std::string(reinterpret_cast<const char*>(cbor.data()), cbor.size()));
REQUIRE(f != nullptr);
CHECK(json::from_cbor(f.get()) == j);
check_unlocked(f.get());
}
static_cast<void>(std::remove(name));
}
+107
View File
@@ -10,6 +10,8 @@
#define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
using nlohmann::json;
TEST_CASE("iterators 1")
@@ -1628,3 +1630,108 @@ TEST_CASE("iterators 1")
}
}
}
TEST_CASE_TEMPLATE("reverse iteration and member access", Json, nlohmann::json, nlohmann::ordered_json)
{
SECTION("array: -- and rbegin/rend")
{
Json j = {1, 2, 3, 4};
auto it = j.end();
std::vector<int> seen;
while (it != j.begin())
{
--it;
seen.push_back(it->template get<int>());
}
CHECK(seen == std::vector<int> {4, 3, 2, 1});
auto it2 = j.end();
auto old = it2--;
CHECK(old == j.end());
CHECK(*it2 == 4);
seen.clear();
for (auto r = j.rbegin(); r != j.rend(); ++r)
{
seen.push_back(r->template get<int>());
}
CHECK(seen == std::vector<int> {4, 3, 2, 1});
seen.clear();
for (auto r = j.crbegin(); r != j.crend(); ++r)
{
seen.push_back(*r);
}
CHECK(seen == std::vector<int> {4, 3, 2, 1});
}
SECTION("object: -- and rbegin/rend")
{
Json j = {{"a", 1}, {"b", 2}, {"c", 3}};
// order of keys depends on the object type; use the forward order as reference
std::vector<std::string> fwd;
for (auto it = j.begin(); it != j.end(); ++it)
{
fwd.push_back(it.key());
}
std::vector<std::string> rev(fwd.rbegin(), fwd.rend());
std::vector<std::string> seen;
auto it = j.end();
while (it != j.begin())
{
--it;
seen.push_back(it.key());
}
CHECK(seen == rev);
auto it2 = j.end();
auto old = it2--;
CHECK(old == j.end());
CHECK(it2.key() == rev.front());
seen.clear();
for (auto r = j.rbegin(); r != j.rend(); ++r)
{
seen.push_back(r.key());
}
CHECK(seen == rev);
}
SECTION("object: operator-> reads and writes the mapped value")
{
Json j = {{"a", 1}, {"s", "str"}};
auto it = j.find("s");
REQUIRE(it != j.end());
CHECK(it->is_string());
CHECK(it->size() == 1);
*it = Json::array({1, 2});
CHECK(it->is_array());
it->push_back(3);
CHECK(j["s"].size() == 3);
const Json& cj = j;
auto cit = cj.find("a");
REQUIRE(cit != cj.end());
CHECK(cit->is_number_integer());
CHECK(cit->template get<int>() == 1);
}
SECTION("primitives: -- and ->")
{
Json j = 42;
auto it = j.end();
--it;
CHECK(it == j.begin());
CHECK(it->template get<int>() == 42);
}
SECTION("comparing iterators of different containers throws")
{
Json a = {1, 2};
Json b = {1, 2};
CHECK_THROWS_WITH_AS(a.begin() == b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator);
CHECK_THROWS_WITH_AS(a.begin() < b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator);
}
}
+37
View File
@@ -2066,3 +2066,40 @@ TEST_CASE("JSON patch - every operation on ordered_json")
}
}
}
TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json, nlohmann::json, nlohmann::ordered_json)
{
const auto roundtrip = [](const Json & a, const Json & b)
{
CAPTURE(a.dump())
CAPTURE(b.dump())
const Json p = Json::diff(a, b);
CHECK(a.patch(p) == b);
CHECK(Json::diff(b, b).empty());
};
SECTION("reordered keys")
{
const Json a = Json::parse(R"({"x":1,"y":2,"z":{"p":1,"q":2}})");
const Json b = Json::parse(R"({"z":{"q":2,"p":1},"y":2,"x":1})");
roundtrip(a, b);
roundtrip(b, a);
}
SECTION("nested add, remove and replace")
{
const Json a = Json::parse(R"({"a":[1,2,{"k":"v","gone":true}],"b":{"c":{"d":1}},"e":"s"})");
const Json b = Json::parse(R"({"a":[1,3,{"k":"w","new":[]}],"b":{"c":{"d":[1,2]}},"f":null})");
roundtrip(a, b);
roundtrip(b, a);
}
SECTION("arrays of different length and type changes")
{
roundtrip(Json::parse("[1,2,3,4,5]"), Json::parse("[1,3]"));
roundtrip(Json::parse("[1]"), Json::parse("[1,[2],{\"a\":3}]"));
roundtrip(Json::parse("[]"), Json::parse("{}"));
roundtrip(Json::parse("{\"a\":[]}"), Json::parse("{\"a\":{}}"));
roundtrip(Json(1), Json::parse("[1]"));
}
}
+245
View File
@@ -23,8 +23,14 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif
#include <cerrno>
#include <cstdlib>
#include <limits>
#include <map>
#include <random>
#include <sstream>
#include <string>
#include <vector>
TEST_CASE("JSON pointers")
{
@@ -1071,3 +1077,242 @@ TEST_CASE("flatten of structured values")
R"({"/z":"s","/a~1b/~0":1,"/a~1b/k":null,"/a/0":1,"/a/1/c":null,"/a/1/b":null,"/a/2/0/x~0~1/0":true,"/a/2/0/x~0~1/1":null,"/a/2/0/w":2})");
}
}
TEST_CASE("flatten of ordered_json")
{
using nlohmann::ordered_json;
SECTION("keeps the document order of the leaves")
{
const ordered_json j = ordered_json::parse(R"({
"z": 1, "a": {"y": [true, null, {}], "b": "x", "": []},
"m~n": 2.5, "o/p": {"k": {}}, "0": [[1], [2, 3]]
})");
const ordered_json flat = j.flatten();
std::vector<std::string> keys;
for (const auto& el : flat.items())
{
keys.push_back(el.key());
}
const std::vector<std::string> expected =
{
"/z", "/a/y/0", "/a/y/1", "/a/y/2", "/a/b", "/a/", "/m~0n", "/o~1p/k", "/0/0/0", "/0/1/0", "/0/1/1"
};
CHECK(keys == expected);
CHECK(flat.size() == expected.size());
CHECK(flat["/m~0n"] == 2.5);
CHECK(flat["/a/y/2"].is_null());
}
SECTION("round-trips and agrees with json")
{
ordered_json j;
for (int i = 0; i < 200; ++i)
{
j["k" + std::to_string(i)] = {{"v", i}, {"l", {i, i + 1, "s"}}};
}
const ordered_json flat = j.flatten();
CHECK(flat.size() == 800);
CHECK(flat.unflatten() == j);
// same entries as for a sorted object
const nlohmann::json plain = nlohmann::json::parse(j.dump());
CHECK(nlohmann::json::parse(flat.dump()) == plain.flatten());
}
SECTION("primitive and empty root")
{
CHECK(ordered_json(1).flatten() == ordered_json({{"", 1}}));
CHECK(ordered_json::array().flatten() == ordered_json({{"", nullptr}}));
}
}
namespace
{
// reference implementation of the array index rules (RFC 6901, Sect. 4) based on strtoull
enum class index_class
{
ok,
leading_zero,
not_a_number,
unresolved,
exceeds_size_type
};
index_class classify_index(const std::string& s, std::size_t& idx)
{
if (s.size() > 1 && s[0] == '0')
{
return index_class::leading_zero;
}
if (!s.empty() && s != "-" && !(s[0] >= '0' && s[0] <= '9'))
{
return index_class::not_a_number;
}
char* end = nullptr;
errno = 0;
const unsigned long long res = std::strtoull(s.c_str(), &end, 10); // NOLINT(runtime/int)
if (end == s.c_str() || errno == ERANGE || static_cast<std::size_t>(end - s.c_str()) != s.size())
{
return index_class::unresolved;
}
if (res >= static_cast<unsigned long long>((std::numeric_limits<std::size_t>::max)())) // NOLINT(runtime/int)
{
return index_class::exceeds_size_type;
}
idx = static_cast<std::size_t>(res);
return index_class::ok;
}
// the observable behavior of json_pointer::at() and contains() for an array index token
void check_index_token(const std::string& token)
{
const json array = {10, 20, 30};
const json::json_pointer ptr("/" + token);
std::size_t idx = 0;
const auto cls = classify_index(token, idx);
const bool exists = cls == index_class::ok && idx < array.size();
CAPTURE(token)
CHECK(array.contains(ptr) == (exists && token != "-"));
if (token == "-")
{
CHECK_THROWS_AS(array.at(ptr), json::out_of_range&);
return;
}
switch (cls)
{
case index_class::leading_zero:
CHECK_THROWS_AS(array.at(ptr), json::parse_error&);
break;
case index_class::not_a_number:
CHECK_THROWS_AS(array.at(ptr), json::parse_error&);
break;
case index_class::unresolved:
case index_class::exceeds_size_type:
CHECK_THROWS_AS(array.at(ptr), json::out_of_range&);
break;
case index_class::ok:
default:
if (exists)
{
CHECK(array.at(ptr) == array[idx]);
}
else
{
CHECK_THROWS_AS(array.at(ptr), json::out_of_range&);
}
break;
}
}
} // namespace
TEST_CASE("JSON pointer array index tokens match strtoull semantics")
{
SECTION("boundary values")
{
const std::string max_digits = std::to_string((std::numeric_limits<std::size_t>::max)());
const std::vector<std::string> tokens =
{
"", "-", "0", "1", "2", "3", "00", "01", "007", "+1", "-1", " 1", "1 ", "1a", "a1", "0x1", "1e1", "1.0",
"999999999", "1000000000", "99999999999999999", "999999999999999999", "1000000000000000000",
"9999999999999999999", "18446744073709551614", max_digits, "18446744073709551616",
"99999999999999999999999", "000000000000000000000"
};
for (const auto& token : tokens)
{
check_index_token(token);
}
}
SECTION("random tokens")
{
std::mt19937 gen(42);
const std::string alphabet = "0123456789012345678901234567890123456789-+ ex";
for (int i = 0; i < 5000; ++i)
{
std::string token(static_cast<std::size_t>(gen() % 23), '0');
for (auto& c : token)
{
c = alphabet[gen() % alphabet.size()];
}
check_index_token(token);
}
}
}
TEST_CASE("JSON pointer operations on pointers with escaped tokens")
{
const std::vector<std::string> strings =
{
"", "/", "//", "/a", "/a/b", "/a~1b", "/~0", "/~01", "/~10", "/a~0~1/b/", "/0/1/2", "/ ", "/a/", "/~1/~0"
};
for (const auto& str : strings)
{
CAPTURE(str)
const json::json_pointer ptr(str);
CHECK(ptr.to_string() == str);
CHECK(ptr / "x" / "y" / std::size_t{3} == json::json_pointer(str + "/x/y/3"));
CHECK(ptr.parent_pointer() == (str.empty() ? ptr : json::json_pointer(str.substr(0, str.rfind('/')))));
// comparison with a string, in place, and with every other pointer
#if !defined(JSON_TEST_DEPRECATED_FUNCTIONS_DELETED)
for (const auto& other : strings)
{
CAPTURE(other)
CHECK((ptr == other) == (str == other));
CHECK((other == ptr) == (str == other));
CHECK((ptr != other) == (str != other));
}
CHECK_THROWS_AS(ptr == "x", json::parse_error&);
CHECK_THROWS_AS(ptr == "/~", json::parse_error&);
CHECK_THROWS_AS(ptr == "/b/~2", json::parse_error&);
#endif
}
}
TEST_CASE("JSON pointer lookups")
{
json j = {{"a", {{"b", {1, 2, 3}}}}, {"", 7}, {"~/", {{"x", nullptr}}}};
const json& cj = j;
CHECK(cj[json::json_pointer("/a/b/2")] == 3);
CHECK(cj.at(json::json_pointer("/a/b/1")) == 2);
CHECK(cj.at(json::json_pointer("/")) == 7);
CHECK(cj.contains(json::json_pointer("/~0~1/x")));
CHECK(!cj.contains(json::json_pointer("/a/c")));
CHECK(cj.value(json::json_pointer("/a/b/0"), 5) == 1);
CHECK(cj.value(json::json_pointer("/a/b/3"), 5) == 5);
CHECK(cj.value(json::json_pointer("/a/zz"), 5) == 5);
CHECK_THROWS_AS(cj.at(json::json_pointer("/a/c")), json::out_of_range&);
CHECK_THROWS_AS(cj.at(json::json_pointer("/a/b/3")), json::out_of_range&);
// non-const access reuses existing entries and creates missing ones
j[json::json_pointer("/a/b/1")] = 20;
j[json::json_pointer("/n/m")] = 1;
CHECK(j["a"]["b"] == json({1, 20, 3}));
CHECK(j["n"]["m"] == 1);
}
TEST_CASE("JSON patch with moves and copies")
{
json doc = {{"a", {{"b", {1, 2, 3}}}}, {"c", {{"d", "x"}}}};
CHECK(doc.patch(json::parse(R"([{"op":"move","from":"/a/b","path":"/c/e"}])")) ==
json::parse(R"({"a":{},"c":{"d":"x","e":[1,2,3]}})"));
CHECK(doc.patch(json::parse(R"([{"op":"copy","from":"/a/b","path":"/c/e"}])")) ==
json::parse(R"({"a":{"b":[1,2,3]},"c":{"d":"x","e":[1,2,3]}})"));
CHECK(doc.patch(json::parse(R"([{"op":"move","from":"/a/b/0","path":"/a/b/2"}])")) ==
json::parse(R"({"a":{"b":[2,3,1]},"c":{"d":"x"}})"));
// a failing move leaves a copy-patched document untouched
CHECK_THROWS_AS(doc.patch(json::parse(R"([{"op":"move","from":"","path":""}])")), json::out_of_range&);
json inplace = doc;
CHECK_THROWS_AS(inplace.patch_inplace(json::parse(R"([{"op":"move","from":"","path":""}])")), json::out_of_range&);
CHECK(inplace == doc);
}
@@ -20,6 +20,7 @@
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstddef>
#include <sstream>
#include <streambuf>
@@ -72,6 +73,40 @@ class no_putback_streambuf : public std::streambuf
std::size_t m_pos = 0;
};
// A streambuf that serves its input in get areas of a fixed size, so that the
// end of a number falls on every possible offset of a get area. The lexer scans
// the get area directly; the stream position must not depend on its size.
class chunked_streambuf : public std::streambuf
{
public:
chunked_streambuf(std::string s, std::size_t chunk_size)
: m_data(std::move(s)), m_chunk_size(chunk_size)
{}
protected:
int_type underflow() override
{
if (gptr() < egptr())
{
return traits_type::to_int_type(*gptr());
}
m_pos += static_cast<std::size_t>(egptr() - eback());
if (m_pos >= m_data.size())
{
setg(nullptr, nullptr, nullptr);
return traits_type::eof();
}
char* const begin = &m_data[m_pos];
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
return traits_type::to_int_type(*gptr());
}
private:
std::string m_data;
std::size_t m_pos = 0;
std::size_t m_chunk_size;
};
// read the characters that are left in a stream
std::string remaining(std::istream& is)
{
@@ -263,4 +298,26 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
}
SECTION("the position does not depend on the get area of the streambuf")
{
const std::vector<std::size_t> chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096};
for (const std::size_t chunk_size : chunk_sizes)
{
CAPTURE(chunk_size)
chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size);
std::istream input(&buf);
json j;
input >> j;
CHECK(j == 12345678901234567890u);
input >> j;
CHECK(j == true);
input >> j;
CHECK(j == "a long string value");
input >> j;
CHECK(j == -1250.0);
CHECK(remaining(input) == ",[1,2]42");
}
}
}
+201
View File
@@ -0,0 +1,201 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <functional>
#include <limits>
#include <string>
#include <vector>
// basic_json with the same type for number_integer_t and number_unsigned_t
// (#2573): every integer is stored as number_integer
namespace
{
template<typename IntegerType>
using same_types_json = nlohmann::json::with_integers_t<IntegerType, IntegerType>;
using json64 = same_types_json<std::int64_t>;
using json32 = same_types_json<std::int32_t>;
} // namespace
TEST_CASE_TEMPLATE("same type for number_integer_t and number_unsigned_t", Json, json64, json32)
{
using integer_t = typename Json::number_integer_t;
using limits = std::numeric_limits<integer_t>;
SECTION("parsing stores integers as number_integer")
{
const Json j = Json::parse("[0, 1, -1, 2147483647, -2147483648]");
for (const auto& element : j)
{
CHECK(element.is_number_integer());
CHECK(!element.is_number_unsigned());
CHECK(element.type() == nlohmann::detail::value_t::number_integer);
}
CHECK(j[1].template get<integer_t>() == 1);
CHECK(j[2].template get<integer_t>() == -1);
CHECK(j[3].template get<integer_t>() == 2147483647);
CHECK(j[4].template get<std::int64_t>() == -2147483648LL);
}
SECTION("integers above the maximum are parsed as floats")
{
// a positive literal is parsed as unsigned first; it must not wrap
// around to a negative number_integer_t
const auto above_max = std::to_string(static_cast<std::uint64_t>(limits::max()) + 1);
const Json j1 = Json::parse(above_max);
CHECK(j1.is_number_float());
CHECK(j1.template get<double>() == static_cast<double>(static_cast<std::uint64_t>(limits::max()) + 1));
const Json j2 = Json::parse("18446744073709551615");
CHECK(j2.is_number_float());
CHECK(j2.template get<double>() == 18446744073709551615.0);
const Json j3 = Json::parse(std::to_string(limits::max()));
CHECK(j3.is_number_integer());
CHECK(j3.template get<integer_t>() == limits::max());
}
SECTION("serialization of the extremes")
{
CHECK(Json(limits::max()).dump() == std::to_string(limits::max()));
// the absolute value of the minimum does not fit into number_unsigned_t
CHECK(Json(limits::min()).dump() == std::to_string(limits::min()));
CHECK(Json::parse(std::to_string(limits::min())) == Json(limits::min()));
CHECK(Json(0).dump() == "0");
}
SECTION("construction from unsigned types")
{
const Json j1 = 5u;
CHECK(j1.is_number_integer());
CHECK(!j1.is_number_unsigned());
CHECK(j1 == 5);
const Json j2 = static_cast<std::uint8_t>(200);
CHECK(j2.is_number_integer());
CHECK(j2.template get<integer_t>() == 200);
const Json j3 = std::vector<unsigned> {1, 2, 3};
CHECK(j3.dump() == "[1,2,3]");
CHECK(j3[0].is_number_integer());
const Json j4 = true;
CHECK(j4.is_boolean());
}
SECTION("value_t::number_unsigned creates a number_integer")
{
const Json j(nlohmann::detail::value_t::number_unsigned);
CHECK(j.is_number_integer());
CHECK(!j.is_number_unsigned());
CHECK(j == 0);
CHECK(j.dump() == "0");
}
SECTION("conversions")
{
Json j = 42;
CHECK(j.template get<unsigned>() == 42u);
CHECK(j.template get<std::uint64_t>() == 42u);
CHECK(j.template get<int>() == 42);
CHECK(j.template get<double>() == 42.0);
CHECK(j.template get_ptr<integer_t*>() != nullptr);
CHECK(*j.template get_ptr<integer_t*>() == 42);
CHECK(j.template get_ref<integer_t&>() == 42);
j.template get_ref<integer_t&>() = 43;
CHECK(j == 43);
integer_t i = 0;
j.get_to(i);
CHECK(i == 43);
const Json object = {{"a", 1}};
CHECK(object.value("a", 0u) == 1u);
CHECK(object.value("b", 7u) == 7u);
}
SECTION("conversion from a basic_json with distinct types")
{
const nlohmann::json j = {1, 2u, -3, 4.5};
CHECK(j[1].is_number_unsigned());
const Json converted = j;
CHECK(converted.dump() == "[1,2,-3,4.5]");
CHECK(converted[1].is_number_integer());
CHECK(!converted[1].is_number_unsigned());
const nlohmann::json back = converted;
CHECK(back == j);
}
SECTION("comparison and hashing")
{
const Json j1 = 1;
const Json j2 = 2u;
CHECK(j1 < j2);
CHECK(j1 != j2);
CHECK(j2 == 2);
CHECK(j2 == 2u);
CHECK(j1 < 1.5);
CHECK(Json(-1) < Json(0u));
CHECK(std::hash<Json> {}(Json(5u)) == std::hash<Json> {}(Json(5)));
}
SECTION("JSON Patch, diff, and flatten")
{
const Json source = Json::parse(R"({"a": [1, 2, 3], "b": 4})");
const Json target = Json::parse(R"({"a": [1, 5], "c": 6})");
CHECK(source.patch(Json::diff(source, target)) == target);
CHECK(source.flatten().unflatten() == source);
}
SECTION("binary formats")
{
const Json j = {{"min", limits::min()}, {"max", limits::max()}, {"zero", 0}, {"small", 100}, {"negative", -100}, {"float", 1.5}};
CHECK(Json::from_cbor(Json::to_cbor(j)) == j);
CHECK(Json::from_msgpack(Json::to_msgpack(j)) == j);
CHECK(Json::from_ubjson(Json::to_ubjson(j)) == j);
CHECK(Json::from_bjdata(Json::to_bjdata(j)) == j);
CHECK(Json::from_bson(Json::to_bson(j)) == j);
CHECK(Json::from_bon8(Json::to_bon8(j)) == j);
for (const auto& element : Json::from_cbor(Json::to_cbor(j)))
{
CHECK(!element.is_number_unsigned());
}
// CBOR unsigned integer 2^64-1 does not fit and is read as float
const std::vector<std::uint8_t> cbor_max = {0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const Json from_cbor = Json::from_cbor(cbor_max);
CHECK(from_cbor.is_number_float());
CHECK(from_cbor.template get<double>() == 18446744073709551615.0);
// MessagePack uint 8 is read as number_integer
const std::vector<std::uint8_t> msgpack_uint8 = {0xCC, 0xC8};
const Json from_msgpack = Json::from_msgpack(msgpack_uint8);
CHECK(from_msgpack.is_number_integer());
CHECK(from_msgpack == 200);
}
}
TEST_CASE("same type for number_integer_t and number_unsigned_t: type traits")
{
CHECK(std::is_same<nlohmann::detail::number_unsigned_overload_t<json64>, nlohmann::detail::same_as_number_integer_type>::value);
CHECK(std::is_same<nlohmann::detail::number_unsigned_overload_t<nlohmann::json>, std::uint64_t>::value);
CHECK(nlohmann::detail::is_compatible_number_integer_type<json64, unsigned>::value);
CHECK(!nlohmann::detail::is_compatible_number_integer_type<nlohmann::json, unsigned>::value);
CHECK(!nlohmann::detail::is_compatible_number_integer_type<json64, bool>::value);
}
+550 -24
View File
@@ -20,8 +20,11 @@
using nlohmann::json;
#include <array>
#include <cstdint>
#include <sstream>
#include <iomanip>
#include <string>
#include <vector>
#include "test_utils.hpp"
@@ -923,66 +926,68 @@ TEST_CASE("serialization boundary values for the string buffer")
// mid-string once fewer than 13 bytes remain (`string_buffer.size() - bytes
// < 13`), 13 being one more than the most a single code point can ever
// write at once (a surrogate pair: two back-to-back "\uXXXX" escapes, 12
// bytes). Every write into string_buffer that this check protects happens
// in steps of 2 (a simple "\\x" escape) or 6 (one "\uXXXX" unit), so
// `bytes` only ever takes even values at the point the check runs - the
// tightest values actually reachable are therefore 498 (512 - 498 == 14,
// one simple escape away from the threshold) and 500 (512 - 500 == 12,
// where the flush fires immediately and resets bytes to 0).
// bytes). Characters below 0x60 (control characters, '"' and '\\') are
// escaped straight into the write buffer and never reach string_buffer, so
// under ensure_ascii it fills in steps of 6 (one "\uXXXX" unit for a
// character below U+10000) or 12 (a surrogate pair). With 2-byte characters
// such as U+00E4, `bytes` takes multiples of 6 at the point the check runs;
// the tightest values reachable are therefore 498 (512 - 498 == 14, after
// 83 characters) and 504 (512 - 504 == 8, after 84 characters, where the
// flush fires immediately and resets bytes to 0).
SECTION("a run of 2-byte escapes lands bytes on, and one step past, the flush threshold")
SECTION("a run of 6-byte escapes lands bytes on, and one step past, the flush threshold")
{
for (const int count :
{
249, 250, 251
83, 84, 85
})
{
CAPTURE(count)
const json j = std::string(static_cast<std::size_t>(count), '\n');
std::string umlauts;
std::string expected = "\"";
for (int i = 0; i < count; ++i)
{
expected += "\\n";
umlauts += "\xC3\xA4";
expected += "\\u00e4";
}
expected += '"';
CHECK(j.dump() == expected);
CHECK(json(umlauts).dump(-1, ' ', true) == expected);
}
}
SECTION("an ASCII prefix leaves the tightest reachable margin before a 12-byte surrogate pair")
SECTION("a prefix leaves the tightest reachable margin before a 12-byte surrogate pair")
{
// U+1F600 (the "\xF0\x9F\x98\x80" UTF-8 bytes) is dumped under
// ensure_ascii as the 12-byte surrogate pair "\ud83d\ude00"; that
// write happens in a single step with no intermediate flush check, so
// it is the write most exposed by an off-by-one in the "< 13" guard.
// A prefix of 249 newlines leaves exactly 14 bytes of headroom
// A prefix of 83 U+00E4 characters leaves exactly 14 bytes of headroom
// (512 - 498), the smallest margin the guard ever actually allows
// into a new code point; 250 newlines instead trigger the guard's own
// into a new code point; 84 characters instead trigger the guard's own
// flush first, so the emoji starts from a freshly emptied (512-byte)
// buffer, and 251 repeats that with one more escape already past the
// buffer, and 85 repeats that with one more escape already past the
// reset. Together they cover the margin the guard allows landing on,
// one step before, and one step after - all must still produce the
// identical, correct escapes.
for (const int prefix_count :
{
249, 250, 251
83, 84, 85
})
{
CAPTURE(prefix_count)
const std::string prefix(static_cast<std::size_t>(prefix_count), '\n');
const std::string emoji = "\xF0\x9F\x98\x80";
const json j = prefix + emoji;
std::string prefix;
std::string expected_prefix;
for (int i = 0; i < prefix_count; ++i)
{
expected_prefix += "\\n";
prefix += "\xC3\xA4";
expected_prefix += "\\u00e4";
}
const std::string emoji = "\xF0\x9F\x98\x80";
const json j = prefix + emoji;
// newline escaping does not depend on ensure_ascii: only the
// emoji differs (raw UTF-8 bytes vs. a \u-escaped surrogate pair)
// without ensure_ascii, the whole string is copied unchanged
std::string expected_raw = "\"";
expected_raw += expected_prefix;
expected_raw += prefix;
expected_raw += emoji;
expected_raw += '"';
std::string expected_ascii = "\"";
@@ -1027,3 +1032,524 @@ TEST_CASE("serialization boundary values for the string buffer")
}
}
}
namespace
{
// appends the escaped form of the ASCII character c to out; written
// independently of the table in serializer::ascii_escapes()
void append_escaped_ascii(std::string& out, const char c, const bool ensure_ascii)
{
const auto byte = static_cast<unsigned char>(c);
switch (byte)
{
case '\b':
out += "\\b";
break;
case '\t':
out += "\\t";
break;
case '\n':
out += "\\n";
break;
case '\f':
out += "\\f";
break;
case '\r':
out += "\\r";
break;
case '"':
out += "\\\"";
break;
case '\\':
out += "\\\\";
break;
default:
if (byte < 0x20 || (ensure_ascii && byte == 0x7F))
{
// byte < 0x80, so the first two hex digits are 0
const char* const hex = "0123456789abcdef";
out += "\\u00";
out += hex[byte >> 4U];
out += hex[byte & 0x0FU];
}
else
{
out += c;
}
break;
}
}
// reference escaper for ASCII strings
std::string reference_escape_ascii(const std::string& s, const bool ensure_ascii)
{
std::string result = "\"";
for (const char c : s)
{
append_escaped_ascii(result, c, ensure_ascii);
}
result += '"';
return result;
}
} // namespace
TEST_CASE("characters below 0x60 are escaped from a table straight into the write buffer")
{
SECTION("every ASCII character at every position")
{
// lengths up to 17 cover the 8-byte strides of the bulk scanner on
// both sides of the character, so the scanner stops at it from every
// offset
for (std::size_t length = 1; length <= 17; ++length)
{
for (std::size_t pos = 0; pos < length; ++pos)
{
for (unsigned int c = 0; c < 0x80; ++c)
{
std::string s(length, 'a');
s[pos] = static_cast<char>(c);
CAPTURE(length)
CAPTURE(pos)
CAPTURE(c)
const json j = s;
CHECK(j.dump(-1, ' ', false) == reference_escape_ascii(s, false));
CHECK(j.dump(-1, ' ', true) == reference_escape_ascii(s, true));
}
}
}
}
SECTION("runs of escaped characters")
{
std::string s;
for (unsigned int c = 0; c < 0x20; ++c)
{
s += static_cast<char>(c);
}
s += R"("\"\)";
CHECK(json(s).dump() == reference_escape_ascii(s, false));
CHECK(json(s).dump(-1, ' ', true) == reference_escape_ascii(s, true));
}
SECTION("escapes straddle the end of the write buffer")
{
// the 8-byte store of a table entry needs room for all 8 bytes, even
// when the escape itself is shorter; a prefix of k plain characters
// shifts the 6-byte escapes across every offset of the 1024-byte
// write buffer
for (std::size_t k = 0; k < 8; ++k)
{
CAPTURE(k)
const std::string s = std::string(k, 'a') + std::string(400, '\x01');
CHECK(json(s).dump() == reference_escape_ascii(s, false));
CHECK(json::array({s, s}).dump() == "[" + reference_escape_ascii(s, false) + "," + reference_escape_ascii(s, false) + "]");
}
}
SECTION("pending \\u escapes of non-ASCII characters are written first")
{
// under ensure_ascii, U+00E4 is \u-escaped into the string buffer;
// a following escape from the table must not overtake it
const json j = "\xC3\xA4\n\xC3\xA4\"\xF0\x9F\x98\x80\\";
CHECK(j.dump(-1, ' ', true) == "\"\\u00e4\\n\\u00e4\\\"\\ud83d\\ude00\\\\\"");
CHECK(j.dump(-1, ' ', false) == "\"\xC3\xA4\\n\xC3\xA4\\\"\xF0\x9F\x98\x80\\\\\"");
}
SECTION("escapes after invalid UTF-8 for every error handler")
{
const json j = "a\xFF\n\xC3\"";
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xFF", json::type_error&);
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"a\\n\\\"\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::ignore) == "\"a\\n\\\"\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"a\xEF\xBF\xBD\\n\xEF\xBF\xBD\\\"\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"a\\ufffd\\n\\ufffd\\\"\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::keep) == "\"a\xFF\\n\xC3\\\"\"");
CHECK(j.dump(-1, ' ', true, json::error_handler_t::keep) == "\"a\xFF\\n\xC3\\\"\"");
}
}
namespace
{
// expected result of dumping a string: the output, or (with
// error_handler_t::strict) the message of the type_error.316 exception
struct expected_dump
{
bool throws = false;
std::string text{}; // NOLINT(readability-redundant-member-init)
};
std::string upper_hex_byte(const unsigned char byte)
{
const char* const hex = "0123456789ABCDEF";
return std::string(1, hex[byte >> 4U]) + hex[byte & 0x0FU];
}
void append_u_escape(std::string& out, const std::uint32_t code_unit)
{
const char* const hex = "0123456789abcdef";
out += "\\u";
for (int shift = 12; shift >= 0; shift -= 4)
{
out += hex[(code_unit >> static_cast<unsigned int>(shift)) & 0x0FU];
}
}
void append_replacement_character(std::string& out, const bool ensure_ascii)
{
out += ensure_ascii ? "\\ufffd" : "\xEF\xBF\xBD";
}
// reference serializer for arbitrary byte strings, written from the documented
// behavior of dump() and independently of serializer::dump_escaped(): UTF-8 is
// decoded strictly (well-formed sequences as in Table 3-7 of the Unicode
// Standard), and each ill-formed sequence is handled as the error handler says
expected_dump reference_dump_string(const std::string& s, const bool ensure_ascii, const json::error_handler_t error_handler)
{
expected_dump result;
std::string& out = result.text;
out = "\"";
// handle the ill-formed bytes s[begin, end), of which s[index] is the byte
// that could not be decoded; returns false if an exception is expected
const auto ill_formed = [&](const std::size_t begin, const std::size_t end, const std::size_t index)
{
switch (error_handler)
{
case json::error_handler_t::strict:
result.throws = true;
out = "[json.exception.type_error.316] invalid UTF-8 byte at index " + std::to_string(index) + ": 0x" + upper_hex_byte(static_cast<unsigned char>(s[index]));
return false;
case json::error_handler_t::ignore:
break;
case json::error_handler_t::replace:
append_replacement_character(out, ensure_ascii);
break;
case json::error_handler_t::keep:
default:
// kept as is, even with ensure_ascii
out.append(s, begin, end - begin);
break;
}
return true;
};
std::size_t i = 0;
while (i < s.size())
{
const auto lead = static_cast<unsigned char>(s[i]);
if (lead < 0x80)
{
append_escaped_ascii(out, s[i], ensure_ascii);
++i;
continue;
}
// number of bytes of the sequence and the range of its second byte
std::size_t length = 0;
std::uint32_t code_point = 0;
unsigned char lower = 0x80;
unsigned char upper = 0xBF;
if (lead >= 0xC2 && lead <= 0xDF)
{
length = 2;
code_point = lead & 0x1FU;
}
else if (lead >= 0xE0 && lead <= 0xEF)
{
length = 3;
code_point = lead & 0x0FU;
lower = (lead == 0xE0) ? 0xA0 : 0x80; // no overlong encodings
upper = (lead == 0xED) ? 0x9F : 0xBF; // no surrogates
}
else if (lead >= 0xF0 && lead <= 0xF4)
{
length = 4;
code_point = lead & 0x07U;
lower = (lead == 0xF0) ? 0x90 : 0x80; // no overlong encodings
upper = (lead == 0xF4) ? 0x8F : 0xBF; // nothing above U+10FFFF
}
else
{
// a byte that cannot start a sequence is consumed
if (!ill_formed(i, i + 1, i))
{
return result;
}
++i;
continue;
}
std::size_t j = i + 1;
for (; j < i + length; ++j)
{
if (j == s.size())
{
// the string ends in the middle of the sequence
switch (error_handler)
{
case json::error_handler_t::strict:
result.throws = true;
out = "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0x" + upper_hex_byte(static_cast<unsigned char>(s.back()));
return result;
case json::error_handler_t::ignore:
break;
case json::error_handler_t::replace:
append_replacement_character(out, ensure_ascii);
break;
case json::error_handler_t::keep:
default:
out.append(s, i, s.size() - i);
break;
}
out += '"';
return result;
}
const auto byte = static_cast<unsigned char>(s[j]);
if (byte < lower || byte > upper)
{
break;
}
code_point = (code_point << 6U) | (byte & 0x3FU);
lower = 0x80;
upper = 0xBF;
}
if (j < i + length)
{
// the sequence was abandoned at s[j]; that byte is read again as
// the start of a new character
if (!ill_formed(i, j, j))
{
return result;
}
i = j;
continue;
}
if (!ensure_ascii)
{
out.append(s, i, length);
}
else if (code_point <= 0xFFFF)
{
append_u_escape(out, code_point);
}
else
{
append_u_escape(out, 0xD800U + ((code_point - 0x10000U) >> 10U));
append_u_escape(out, 0xDC00U + ((code_point - 0x10000U) & 0x3FFU));
}
i = j;
}
out += '"';
return result;
}
std::string hex_dump(const std::string& s)
{
std::string result;
for (const char c : s)
{
result += upper_hex_byte(static_cast<unsigned char>(c));
result += ' ';
}
return result;
}
// compares dump() of s with the reference for every error handler, with
// ensure_ascii on and off; CHECKs only on a mismatch to keep the sweep fast;
// error_handler_t::strict can be skipped where the position of the string
// buffer does not matter, as throwing is slow on some platforms
void check_dump_against_reference(const std::string& s, const bool with_strict = true)
{
const json j = s;
const std::array<json::error_handler_t, 4> error_handlers =
{
{
json::error_handler_t::strict, json::error_handler_t::ignore,
json::error_handler_t::replace, json::error_handler_t::keep
}
};
for (const auto error_handler : error_handlers)
{
if (!with_strict && error_handler == json::error_handler_t::strict)
{
continue;
}
for (const bool ensure_ascii :
{
false, true
})
{
const expected_dump expected = reference_dump_string(s, ensure_ascii, error_handler);
expected_dump actual;
int id = 0;
try
{
actual.text = j.dump(-1, ' ', ensure_ascii, error_handler);
}
catch (const json::exception& e)
{
actual.throws = true;
actual.text = e.what();
id = e.id;
}
if (actual.throws != expected.throws || actual.text != expected.text || (actual.throws && id != 316))
{
const std::string input = hex_dump(s);
CAPTURE(input)
CAPTURE(ensure_ascii)
CAPTURE(static_cast<int>(error_handler))
CHECK(actual.throws == expected.throws);
CHECK(actual.text == expected.text);
CHECK((!actual.throws || id == 316));
}
}
}
}
} // namespace
TEST_CASE("invalid UTF-8 at every position is handled like the reference")
{
// ill-formed byte sequences; those that end in a truncated sequence are
// also cut short by the filler that follows, or by the end of the string
const std::vector<std::string> patterns =
{
// stray continuation bytes
"\x80", "\xBF",
// bytes that cannot start a sequence
"\xC0", "\xC1", "\xF5", "\xF6", "\xF7", "\xF8", "\xF9", "\xFA", "\xFB", "\xFC", "\xFD", "\xFE", "\xFF",
// overlong encodings
"\xC0\xAF", "\xE0\x80\x80", "\xF0\x80\x80\x80",
// UTF-16 surrogates
"\xED\xA0\x80", "\xED\xBF\xBF",
// a code point above U+10FFFF
"\xF4\x90\x80\x80",
// sequences cut short by ASCII, a control character, a quote, a
// backslash, DEL, or the lead byte of a well-formed character
"\xC3" "a", "\xE2\x82" "\n", "\xE2\x82\"", "\xF0\x9F\x98\\", "\xC3\x7F", "\xC3\xC3\xA4", "\xE2\xF0\x9F\x98\x80",
// truncated sequences
"\xC3", "\xE2\x82", "\xF0\x9F\x98"
};
// well-formed characters of 1, 2, 3, and 4 bytes
const std::vector<std::string> fillers = {"a", "\xC3\xA4", "\xE2\x82\xAC", "\xF0\x9F\x98\x80"};
const auto make_string = [](const std::string & filler, const std::size_t before, const std::string & pattern, const std::size_t after)
{
std::string s;
for (std::size_t k = 0; k < before; ++k)
{
s += filler;
}
s += pattern;
for (std::size_t k = 0; k < after; ++k)
{
s += filler;
}
return s;
};
SECTION("the reference")
{
// the reference itself, against hand-written results
const std::string s = "a\xFF\n\xC3\"\xE0\x80\xF0\x9F\x98\x80\xE2\x82";
CHECK(reference_dump_string(s, false, json::error_handler_t::strict).text == "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0xFF");
CHECK(reference_dump_string(s.substr(2), false, json::error_handler_t::strict).text == "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0x22");
CHECK(reference_dump_string("\xF0\x9F\x98\x80\xE2\x82", true, json::error_handler_t::strict).text == "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0x82");
CHECK(reference_dump_string(s, false, json::error_handler_t::ignore).text == "\"a\\n\\\"\xF0\x9F\x98\x80\"");
CHECK(reference_dump_string(s, true, json::error_handler_t::ignore).text == "\"a\\n\\\"\\ud83d\\ude00\"");
CHECK(reference_dump_string(s, false, json::error_handler_t::replace).text == "\"a\xEF\xBF\xBD\\n\xEF\xBF\xBD\\\"\xEF\xBF\xBD\xEF\xBF\xBD\xF0\x9F\x98\x80\xEF\xBF\xBD\"");
CHECK(reference_dump_string(s, true, json::error_handler_t::replace).text == "\"a\\ufffd\\n\\ufffd\\\"\\ufffd\\ufffd\\ud83d\\ude00\\ufffd\"");
CHECK(reference_dump_string(s, false, json::error_handler_t::keep).text == "\"" + std::string("a\xFF\\n\xC3\\\"\xE0\x80\xF0\x9F\x98\x80\xE2\x82") + "\"");
CHECK(reference_dump_string(s, true, json::error_handler_t::keep).text == "\"" + std::string("a\xFF\\n\xC3\\\"\xE0\x80\\ud83d\\ude00\xE2\x82") + "\"");
check_dump_against_reference(s);
}
SECTION("short strings")
{
// strings of up to 17 bytes put the pattern at every offset of an
// 8-byte stride of the bulk scanners, with whole strides on both
// sides; lengths around 24, 32, and 40 bytes cross the 16- and 32-byte
// strides; error_handler_t::strict is checked for up to 17 bytes of
// ASCII and 8 bytes of other characters only, as throwing is slow on
// some platforms
for (const auto& filler : fillers)
{
for (std::size_t length = 0; length * filler.size() <= 40; ++length)
{
const std::size_t bytes = length * filler.size();
if (filler.size() == 1 && bytes > 17 && (bytes + 1) % 8 > 2)
{
continue;
}
for (std::size_t pos = 0; pos <= length; ++pos)
{
for (const auto& pattern : patterns)
{
check_dump_against_reference(make_string(filler, pos, pattern, length - pos), bytes <= (filler.size() == 1 ? 17 : 8));
}
}
}
}
}
SECTION("strings crossing the end of the string buffer")
{
// characters that are not copied in bulk go through a 512-byte string
// buffer that is written out once fewer than 13 bytes are left; with
// ensure_ascii, each 2-byte character takes 6 bytes of it and each
// 4-byte character 12, so the pattern meets the end of the buffer after
// about 84 or 42 characters (3-byte characters behave like 2-byte
// ones); plain ASCII, copied in bulk, is checked around 512 bytes;
// positions further away are sampled; error_handler_t::strict throws
// at the first ill-formed byte, regardless of the string buffer, and is
// covered above
for (const auto& filler : fillers)
{
if (filler.size() == 3)
{
continue;
}
const std::size_t escaped_size = filler.size() == 1 ? 1 : filler.size() * 3;
const std::size_t middle = 506 / escaped_size;
const std::size_t window = filler.size() == 1 ? 8 : 16;
for (std::size_t pos = 0; pos <= middle + window; pos += (pos + window >= middle) ? 1 : 29)
{
for (const std::size_t after :
{
std::size_t{0}, std::size_t{12}
})
{
for (const auto& pattern : patterns)
{
check_dump_against_reference(make_string(filler, pos, pattern, after), false);
}
}
}
}
}
SECTION("repeated invalid sequences at every offset of the string buffer")
{
// ill-formed sequences that are kept or replaced fill the string
// buffer without a bulk copy in between; a prefix of 0 to 12 bytes
// 0xFF (kept as 1 byte, replaced by 3 or 6) shifts them across every
// offset of the end of the buffer
for (const auto& pattern : patterns)
{
for (std::size_t shift = 0; shift <= 12; ++shift)
{
std::string s(shift, '\xFF');
while (s.size() < 600)
{
s += pattern;
}
check_dump_against_reference(s, shift == 0);
check_dump_against_reference(s + "\xF0\x9F\x98\x80", false);
}
}
}
}
+417
View File
@@ -0,0 +1,417 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
// the library undefines its configuration macros at the end of the header
#if defined(JSON_DISABLE_STREAM_BULK_SCAN) && JSON_DISABLE_STREAM_BULK_SCAN
#define TEST_STREAM_BULK_SCAN 0
#else
#define TEST_STREAM_BULK_SCAN 1
#endif
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <istream>
#include <random>
#include <sstream>
#include <streambuf>
#include <string>
#include <utility>
#include <vector>
// The lexer scans the characters in the get area of a std::istream's
// streambuf directly instead of reading them one at a time. These tests pin
// that this is not observable: for any get area size - including a streambuf
// without a get area, which still takes the character-at-a-time path - the
// parsed values, the exception messages (including the "last read" token),
// the stream state, and the characters left in the stream are the same. See
// unit-stream-bulk-scan-disabled.cpp for JSON_DISABLE_STREAM_BULK_SCAN.
namespace
{
// A streambuf that serves its input in get areas of a fixed size, so that
// tokens, numbers, UTF-8 sequences, and indentation cross the end of the get
// area at every possible offset. With a size of 0, it keeps no get area at all
// and serves every character from underflow()/uflow().
class chunked_streambuf : public std::streambuf
{
public:
chunked_streambuf(std::string s, std::size_t chunk_size)
: m_data(std::move(s)), m_chunk_size(chunk_size)
{}
protected:
int_type underflow() override
{
if (m_chunk_size == 0)
{
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof();
}
if (gptr() < egptr())
{
return traits_type::to_int_type(*gptr());
}
// the previous get area has been consumed
m_pos += static_cast<std::size_t>(egptr() - eback());
if (m_pos >= m_data.size())
{
setg(nullptr, nullptr, nullptr);
return traits_type::eof();
}
char* const begin = &m_data[m_pos];
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
return traits_type::to_int_type(*gptr());
}
int_type uflow() override
{
if (m_chunk_size == 0)
{
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof();
}
return std::streambuf::uflow();
}
private:
std::string m_data;
std::size_t m_pos = 0;
std::size_t m_chunk_size;
};
// how a test reads from the stream
enum class read_mode
{
extract, // up to four times operator>>, as for concatenated values
parse, // json::parse(stream)
parse_lenient, // json::parse(stream) with comments
};
// Read @a input from a chunked_streambuf and describe everything observable:
// the values or the exception, the stream state after each value, and the
// characters left in the stream.
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode)
{
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
std::string result;
for (int i = 0; i < 4; ++i)
{
json j;
try
{
if (mode == read_mode::extract)
{
is >> j;
}
else
{
j = json::parse(is, nullptr, true, mode == read_mode::parse_lenient);
}
result += "value " + j.dump() + "; state " + std::to_string(static_cast<unsigned int>(is.rdstate())) + "\n";
}
catch (const json::exception& e)
{
result += std::string("exception ") + e.what() + "\n";
break;
}
if (mode != read_mode::extract)
{
break;
}
is.clear();
}
is.clear();
result += "remaining ";
char c = 0;
while (is.get(c))
{
result += c;
}
return result;
}
// get area sizes that put every boundary everywhere in short inputs, plus one
// that holds most inputs completely
std::vector<std::size_t> chunk_sizes()
{
return {1, 2, 3, 4, 5, 7, 8, 9, 13, 16, 64, 4096};
}
// valid and invalid inputs covering every token type and every bulk path:
// strings (plain runs, escapes, UTF-8), numbers (all grammar parts, too large
// for an integer), and indentation
std::vector<std::string> corpus()
{
return
{
// numbers, terminated in every way
"0", "-0", "123", "-123", "1.5", "-1.5e+10", "2E-3", "123456789012345678901234567890",
"18446744073709551615", "18446744073709551616", "-9223372036854775809",
"123 456", "123,456", "123]", "1true", "42\nfoo", "1.25e3}", "7\t8\r9",
// malformed numbers
"1.", "-", "1e", "1e+", "01", "-a", "1.x", "1ee5",
// strings
R"("")", R"("abc")", R"("a long string with a run of ordinary characters to scan in bulk")",
R"("escapes \" \\ \/ \b \f \n \r \t é 😀")",
"\"UTF-8: \xc3\xa9 \xe2\x82\xac \xf0\x9f\x98\x80 done\"",
// malformed strings
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
R"("\ud800 lone surrogate")",
// literals
"true", "false", "null", "tru", "nul", "falsey",
// structures
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
// pretty-printed
"{\n \"key\": [\n 1,\n -2.5,\n \"three\"\n ]\n}",
"[\n \"deep\"\n ]",
"{\n \"a\": 1,\n \"b\": 2x\n}",
// concatenated values and whitespace
"1 2 3", R"("a""b")", "[1][2]", "{}{}", " \n\t 42 \n\t ", "",
// comments (only accepted by parse_lenient)
"/* comment */ 1", "// comment\n[1, /* c */ 2]",
// byte order mark
"\xef\xbb\xbf[1]",
};
}
} // namespace
TEST_CASE("parsing from a std::istream scans the get area in bulk")
{
SECTION("results do not depend on the get area")
{
const std::vector<read_mode> read_modes = {read_mode::extract, read_mode::parse, read_mode::parse_lenient};
for (const auto& input : corpus())
{
for (const read_mode mode : read_modes)
{
// no get area: the character-at-a-time path
const std::string expected = observe(input, 0, mode);
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(input)
CAPTURE(chunk_size)
CAPTURE(static_cast<int>(mode))
CHECK(observe(input, chunk_size, mode) == expected);
}
}
}
}
SECTION("values are the same as when parsing from a string")
{
const std::string input = R"({"name": "nlohmann::json", "numbers": [1, -2, 3.5, 4e10, 18446744073709551615],)"
"\n \"text\": \"caf\xc3\xa9 \\u00e9 \\\"quoted\\\"\", \"nested\": [[{}], [[]]], \"flags\": [true, false, null]}";
const json expected = json::parse(input);
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
CHECK(json::parse(is) == expected);
}
std::istringstream ss(input);
CHECK(json::parse(ss) == expected);
}
SECTION("a token that spans several get areas is reported completely")
{
// the error is found after the string has crossed many get areas; the
// token string holds everything since the start of the last token
const std::string input = R"(["this string is longer than every get area", tru])";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 1, column 50: syntax error while parsing value - invalid literal; last read: '\"this string is longer than every get area\", tru]'",
json::parse_error&);
}
const std::string unterminated = R"("this string is longer than every get area)";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(unterminated, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 1, column 43: syntax error while parsing value - invalid string: missing closing quote; last read: '\"this string is longer than every get area'",
json::parse_error&);
}
}
SECTION("an error before the first token reports everything read")
{
chunked_streambuf buf(" \n x", 2);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - invalid literal; last read: ' <U+000A> x'",
json::parse_error&);
}
SECTION("lines and columns are counted across get areas")
{
const std::string input = "[\n 1,\n \"two\",\n 3.0,\n x\n]";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
CHECK_THROWS_WITH_AS(is >> j,
"[json.exception.parse_error.101] parse error at line 5, column 5: syntax error while parsing value - invalid literal; last read: '3.0,<U+000A> x'",
json::parse_error&);
}
}
SECTION("many values from one stream")
{
std::string input;
for (int i = 0; i < 200; ++i)
{
input += R"({"id":)" + std::to_string(i) + R"(,"name":"user)" + std::to_string(i) + R"(","score":)" + std::to_string(i) + ".5}\n";
input += std::to_string(i * 7919) + "\n";
}
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
int count = 0;
while (!(is >> std::ws).eof())
{
is >> j;
if (count % 2 == 0)
{
CHECK(j["id"] == count / 2);
}
else
{
CHECK(j == (count / 2) * 7919);
}
++count;
}
CHECK(count == 400);
}
}
SECTION("the stream can be used after parsing")
{
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(R"({"a": "some text"} trailing words 42)", chunk_size);
std::istream is(&buf);
json j;
is >> j;
CHECK(j == json({{"a", "some text"}}));
std::string word;
int number = 0;
is >> word;
CHECK(word == "trailing");
is >> word >> number;
CHECK(word == "words");
CHECK(number == 42);
}
}
SECTION("std::stringstream and std::ifstream")
{
const std::string input = R"([1, 2.5, "three", {"four": [true, null]}] 5)";
std::stringstream ss(input);
json j1;
json j2;
ss >> j1 >> j2;
CHECK(j1 == json::parse(R"([1, 2.5, "three", {"four": [true, null]}])"));
CHECK(j2 == 5);
CHECK(ss.eof());
// a file name of its own for every run: the test runs for several C++
// standards and configurations at the same time, in the same directory
std::random_device rd;
const std::string temp_name = "unit-stream-bulk-scan-" + std::to_string(rd()) + ".tmp";
const char* const filename = temp_name.c_str();
{
std::ofstream file(filename);
file << input;
}
{
std::ifstream file(filename);
json k1;
json k2;
file >> k1 >> k2;
CHECK(k1 == j1);
CHECK(k2 == j2);
}
static_cast<void>(std::remove(filename));
}
SECTION("binary formats read from a stream")
{
const json j = {{"string", std::string(300, 'x')}, {"array", {1, 2, 3}}, {"number", 1.5}};
const std::vector<std::uint8_t> cbor = json::to_cbor(j);
const std::string bytes(reinterpret_cast<const char*>(cbor.data()), cbor.size());
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(bytes, chunk_size);
std::istream is(&buf);
CHECK(json::from_cbor(is) == j);
}
}
#if TEST_STREAM_BULK_SCAN
SECTION("the adapter keeps no token unless the lexer starts one")
{
// the binary readers read through the same adapter, but never start a
// token; refilling the get area must then not collect the input
chunked_streambuf buf(std::string(1000, 'x'), 7);
std::istream is(&buf);
nlohmann::detail::input_stream_adapter ia(is);
for (int i = 0; i < 1000; ++i)
{
CHECK(ia.get_character() == 'x');
}
std::vector<char> token;
ia.copy_token(token, false);
CHECK(token.empty());
}
#endif
SECTION("SAX parsing from a stream")
{
const std::string input = R"({"a": [1, "two", 3.5]} rest)";
for (const std::size_t chunk_size : chunk_sizes())
{
CAPTURE(chunk_size)
chunked_streambuf buf(input, chunk_size);
std::istream is(&buf);
json j;
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::string_input_adapter_type> sax(j);
CHECK(json::sax_parse(is, &sax, json::input_format_t::json, false));
CHECK(j == json::parse(R"({"a": [1, "two", 3.5]})"));
}
}
}
+135
View File
@@ -0,0 +1,135 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <string>
// The exceptions in hot core functions are constructed out of line. These tests pin the
// exact what() texts of the sites that moved, so the refactoring cannot change them.
#define CHECK_MSG(expr, msg) CHECK_THROWS_WITH(expr, msg)
TEST_CASE("exception messages of out-of-line throw sites")
{
json arr = {1, 2, 3};
json arr2 = {4, 5};
json obj = {{"a", 1}, {"b", 2}};
json obj2 = {{"c", 3}};
json num = 1;
SECTION("iterators")
{
CHECK_MSG((void)(arr.begin() == arr2.begin()), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers");
CHECK_MSG((void)(arr.begin() < arr2.begin()), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers");
CHECK_MSG((void)(obj.begin() < obj.end()), "[json.exception.invalid_iterator.213] cannot compare order of object iterators");
CHECK_MSG(obj.begin() += 1, "[json.exception.invalid_iterator.209] cannot use offsets with object iterators");
CHECK_MSG((void)(obj.begin() - 1), "[json.exception.invalid_iterator.209] cannot use offsets with object iterators");
CHECK_MSG((void)obj.begin()[0], "[json.exception.invalid_iterator.208] cannot use operator[] for object iterators");
CHECK_MSG((void)arr.begin().key(), "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
json null;
CHECK_MSG((void)*null.end(), "[json.exception.invalid_iterator.214] cannot get value");
}
SECTION("constructors")
{
CHECK_MSG(json(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.201] iterators are not compatible");
CHECK_MSG(json(num.begin() + 1, num.end() + 1), "[json.exception.invalid_iterator.204] iterators out of range");
CHECK_MSG(json(json::initializer_list_t{1, 2}, false, json::value_t::object), "[json.exception.type_error.301] cannot create object from initializer list");
}
SECTION("get_ref and operator[]")
{
CHECK_MSG((void)num.get_ref<std::string&>(), "[json.exception.type_error.303] incompatible ReferenceType for get_ref, actual type is number");
CHECK_MSG((void)num[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with number");
}
SECTION("at")
{
const json carr = arr;
const json cobj = obj;
CHECK_MSG((void)arr.at(static_cast<json::size_type>(10)), "[json.exception.out_of_range.401] array index 10 is out of range");
CHECK_MSG((void)carr.at(static_cast<json::size_type>(3)), "[json.exception.out_of_range.401] array index 3 is out of range");
CHECK_MSG((void)num.at(static_cast<json::size_type>(0)), "[json.exception.type_error.304] cannot use at() with number");
CHECK_MSG((void)obj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
CHECK_MSG((void)cobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
CHECK_MSG((void)obj.at(std::string("zz")), "[json.exception.out_of_range.403] key 'zz' not found");
CHECK_MSG((void)num.at("a"), "[json.exception.type_error.304] cannot use at() with number");
nlohmann::ordered_json oobj = {{"a", 1}};
const nlohmann::ordered_json coobj = oobj;
CHECK_MSG((void)oobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
CHECK_MSG((void)coobj.at("z"), "[json.exception.out_of_range.403] key 'z' not found");
CHECK_MSG((void)coobj.at(std::string("zz")), "[json.exception.out_of_range.403] key 'zz' not found");
}
SECTION("erase")
{
CHECK_MSG(arr.erase(arr2.begin()), "[json.exception.invalid_iterator.202] iterator does not fit current value");
CHECK_MSG(num.erase(num.end()), "[json.exception.invalid_iterator.205] iterator out of range");
CHECK_MSG(arr.erase(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.203] iterators do not fit current value");
CHECK_MSG(num.erase(num.begin() + 1, num.end()), "[json.exception.invalid_iterator.204] iterators out of range");
CHECK_MSG(arr.erase(static_cast<json::size_type>(10)), "[json.exception.out_of_range.401] array index 10 is out of range");
}
SECTION("insert")
{
CHECK_MSG(arr.insert(arr2.begin(), 1), "[json.exception.invalid_iterator.202] iterator does not fit current value");
CHECK_MSG(arr.insert(arr2.begin(), 2, 1), "[json.exception.invalid_iterator.202] iterator does not fit current value");
CHECK_MSG(arr.insert(arr2.begin(), arr2.begin(), arr2.end()), "[json.exception.invalid_iterator.202] iterator does not fit current value");
CHECK_MSG(arr.insert(arr.begin(), arr.begin(), arr2.end()), "[json.exception.invalid_iterator.210] iterators do not fit");
CHECK_MSG(arr.insert(arr.begin(), arr.begin(), arr.end()), "[json.exception.invalid_iterator.211] passed iterators may not belong to container");
CHECK_MSG(arr.insert(arr.begin(), obj.begin(), obj.end()), "[json.exception.invalid_iterator.202] iterators first and last must point to arrays");
CHECK_MSG(arr.insert(arr2.begin(), {1, 2}), "[json.exception.invalid_iterator.202] iterator does not fit current value");
CHECK_MSG(obj.insert(obj2.begin(), arr.end()), "[json.exception.invalid_iterator.210] iterators do not fit");
CHECK_MSG(obj.insert(arr.begin(), arr.end()), "[json.exception.invalid_iterator.202] iterators first and last must point to objects");
CHECK_MSG(obj.update(arr.begin(), arr2.end()), "[json.exception.invalid_iterator.210] iterators do not fit");
}
SECTION("patch")
{
const auto patch_msg = [](const char* doc, const char* patch)
{
try
{
json d = json::parse(doc);
d.patch(json::parse(patch));
}
catch (const json::exception& e)
{
// with JSON_DIAGNOSTICS, what() names the offending value like "(/0) "; drop it
std::string msg = e.what();
const auto pos = msg.find(" (/");
if (pos != std::string::npos)
{
msg.erase(pos + 1, msg.find(") ", pos) - pos + 1);
}
return msg;
}
return std::string();
};
CHECK(patch_msg("{}", "{}") == "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects");
CHECK(patch_msg("{}", "[1]") == "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects");
CHECK(patch_msg("{}", "[{}]") == "[json.exception.parse_error.105] parse error: operation must have member 'op'");
CHECK(patch_msg("{}", "[{\"op\":1}]") == "[json.exception.parse_error.105] parse error: operation must have string member 'op'");
CHECK(patch_msg("{}", "[{\"op\":\"add\"}]") == "[json.exception.parse_error.105] parse error: operation 'add' must have member 'path'");
CHECK(patch_msg("{}", "[{\"op\":\"add\",\"path\":1}]") == "[json.exception.parse_error.105] parse error: operation 'add' must have string member 'path'");
CHECK(patch_msg("{}", "[{\"op\":\"foo\",\"path\":\"\"}]") == "[json.exception.parse_error.105] parse error: operation value 'foo' is invalid");
CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"remove\",\"path\":\"/b\"}]") == "[json.exception.out_of_range.403] key 'b' not found");
CHECK(patch_msg("[1]", "[{\"op\":\"add\",\"path\":\"/5\",\"value\":0}]") == "[json.exception.out_of_range.401] array index 5 is out of range");
CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"add\",\"path\":\"/a/b\",\"value\":0}]") == "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type number, but must be an object or array");
CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"remove\",\"path\":\"/a/b\"}]") == "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array");
CHECK(patch_msg("{\"a\":{}}", "[{\"op\":\"move\",\"from\":\"/a\",\"path\":\"/a/b\"}]") == "[json.exception.out_of_range.414] cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/b'");
CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"test\",\"path\":\"/a\",\"value\":2}]") == "[json.exception.other_error.501] unsuccessful: {\"op\":\"test\",\"path\":\"/a\",\"value\":2}");
CHECK(patch_msg("{\"a\":1}", "[{\"op\":\"move\",\"path\":\"/a\"}]") == "[json.exception.parse_error.105] parse error: operation 'move' must have member 'from'");
}
}
+7 -17
View File
@@ -21,11 +21,8 @@ using nlohmann::detail::dtoa_impl::reinterpret_bits;
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iomanip>
#include <limits>
#include <locale>
#include <random>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
@@ -689,20 +686,13 @@ void check_shortest(double v)
const std::string& digits = de.first;
if (digits.size() > 1)
{
// the decimals of one digit fewer next to the value
// (a stream rather than snprintf("%.*e"), whose output GCC cannot bound)
std::ostringstream shorter;
shorter.imbue(std::locale::classic());
shorter << std::scientific << std::setprecision(static_cast<int>(digits.size()) - 2) << v;
const auto near = digits_and_exponent(shorter.str());
// as an integer with digits.size() - 1 digits
std::string m = near.first;
int e = near.second;
while (m.size() < digits.size() - 1)
{
m += '0';
--e;
}
// the decimals of one digit fewer next to the value: v lies within
// half a unit of the last digit of text, so the closest ones are the
// truncated digits and their successor
// (not a stream with std::setprecision(digits.size() - 2): the
// standard library of MSVC 2017 ignores a precision of 0)
const std::string m = digits.substr(0, digits.size() - 1);
const int e = de.second + 1;
const std::uint64_t mid = std::stoull(m);
for (const std::uint64_t candidate :
{
+257
View File
@@ -0,0 +1,257 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <array>
#include <deque>
#include <forward_list>
#include <list>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
#include <valarray>
#include <vector>
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1)
#if __has_include(<filesystem>)
#include <filesystem>
#define TO_JSON_RVALUE_HAS_FS 1
#endif
#endif
namespace
{
// element type that counts how it is converted to JSON: a conversion from an
// lvalue copies, a conversion from an rvalue moves
struct tracked
{
std::string payload;
bool moved_from = false;
explicit tracked(std::string p) : payload(std::move(p)) {}
tracked(const tracked&) = default;
tracked& operator=(const tracked&) = default;
tracked(tracked&& other) noexcept : payload(std::move(other.payload)), moved_from(other.moved_from)
{
other.moved_from = true;
}
tracked& operator=(tracked&& other) noexcept
{
payload = std::move(other.payload);
moved_from = other.moved_from;
other.moved_from = true;
return *this;
}
~tracked() = default;
};
int tracked_copies = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage)
int tracked_moves = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage)
void to_json(json& j, const tracked& t)
{
++tracked_copies;
j = t.payload;
}
void to_json(json& j, tracked&& t)
{
++tracked_moves;
j = std::move(t.payload);
t.moved_from = true;
}
// user type with its own rvalue-aware serializer
struct udt_container
{
std::vector<int> values;
};
} // namespace
namespace nlohmann
{
template<>
struct adl_serializer<udt_container>
{
static void to_json(json& j, const udt_container& c)
{
j = c.values;
j.push_back("copied");
}
static void to_json(json& j, udt_container&& c)
{
j = std::move(c.values);
j.push_back("moved");
}
};
} // namespace nlohmann
TEST_CASE("to_json from rvalue containers")
{
const std::string long_a(64, 'a');
const std::string long_b(64, 'b');
SECTION("vector<std::string>: elements are moved")
{
std::vector<std::string> v{long_a, long_b};
const json j = std::move(v);
CHECK(j == json::array({long_a, long_b}));
for (const auto& s : v) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
{
CHECK(s.empty()); // long strings leave the source element empty when moved
}
}
SECTION("vector<std::string>: lvalues are copied and left untouched")
{
const std::vector<std::string> v{long_a, long_b};
const json j = v;
CHECK(j == json::array({long_a, long_b}));
CHECK(v[0] == long_a);
CHECK(v[1] == long_b);
std::vector<std::string> v2{long_a, long_b};
const json j2 = v2;
CHECK(j2 == j);
CHECK(v2[0] == long_a);
CHECK(v2[1] == long_b);
}
SECTION("tracked elements: rvalue moves, lvalue copies")
{
tracked_copies = 0;
tracked_moves = 0;
std::vector<tracked> v;
v.emplace_back("x");
v.emplace_back("y");
const json copy = v;
CHECK(copy == json::array({"x", "y"}));
CHECK(tracked_copies == 2);
CHECK(tracked_moves == 0);
CHECK_FALSE(v[0].moved_from);
const json moved = std::move(v);
CHECK(moved == copy);
CHECK(tracked_copies == 2);
CHECK(tracked_moves == 2);
}
SECTION("const rvalue is copied")
{
tracked_copies = 0;
tracked_moves = 0;
const std::vector<tracked> v(2, tracked("z"));
const json j = std::move(v); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
CHECK(j == json::array({"z", "z"}));
CHECK(tracked_copies == 2);
CHECK(tracked_moves == 0);
}
SECTION("other sequence containers")
{
CHECK(json(std::deque<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
CHECK(json(std::list<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
CHECK(json(std::forward_list<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
CHECK(json(std::array<std::string, 2> {{long_a, long_b}}) == json::array({long_a, long_b}));
CHECK(json(std::set<std::string> {long_a, long_b}) == json::array({long_a, long_b}));
CHECK(json(std::vector<bool> {true, false}) == json::array({true, false}));
CHECK(json(std::vector<int> {1, 2, 3}) == json::array({1, 2, 3}));
CHECK(json(std::vector<std::vector<std::string>> {{long_a}, {long_b}}) == json::array({json::array({long_a}), json::array({long_b})}));
CHECK(json(std::vector<json> {1, "a"}) == json::array({1, "a"}));
}
SECTION("valarray")
{
std::valarray<int> va(3);
va[0] = 1;
va[1] = 2;
va[2] = 3;
CHECK(json(std::move(va)) == json::array({1, 2, 3})); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
CHECK(json(std::valarray<int>(2)) == json::array({0, 0}));
}
SECTION("array_t and json arrays")
{
json::array_t arr{1, 2};
CHECK(json(std::move(arr)) == json::array({1, 2}));
json a = json::array({1, 2});
const json b = std::move(a);
CHECK(b == json::array({1, 2}));
}
SECTION("object-like containers")
{
std::map<std::string, std::string> m{{"a", long_a}, {"b", long_b}};
const json j = std::move(m);
CHECK(j == json({{"a", long_a}, {"b", long_b}}));
for (const auto& kv : m) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
{
CHECK(kv.second.empty()); // values moved, keys (const) copied
}
const std::map<std::string, std::string> cm{{"a", long_a}};
const json jc = cm;
CHECK(jc["a"] == long_a);
CHECK(cm.at("a") == long_a);
std::unordered_map<std::string, std::string> um{{"a", long_a}};
CHECK(json(std::move(um)) == json({{"a", long_a}}));
std::multimap<std::string, int> mm{{"a", 1}};
CHECK(json(std::move(mm)) == json({{"a", 1}}));
tracked_copies = 0;
tracked_moves = 0;
std::map<std::string, tracked> mt;
mt.emplace("k", tracked("v"));
const json jt = std::move(mt);
CHECK(jt == json({{"k", "v"}}));
CHECK(tracked_moves == 1);
CHECK(tracked_copies == 0);
json::object_t obj{{"a", 1}};
CHECK(json(std::move(obj)) == json({{"a", 1}}));
}
SECTION("user type with its own to_json overloads")
{
udt_container c{{1, 2}};
const json lv = c;
CHECK(lv == json::array({1, 2, "copied"}));
const json rv = std::move(c);
CHECK(rv == json::array({1, 2, "moved"}));
}
SECTION("assignment and implicit conversion")
{
std::vector<std::string> v{long_a};
json j;
j = std::move(v);
CHECK(j == json::array({long_a}));
json k = {std::vector<std::string>{long_b}};
CHECK(k.size() == 1);
}
#ifdef TO_JSON_RVALUE_HAS_FS
SECTION("std::filesystem::path rvalue is still a string")
{
CHECK(json(std::filesystem::path("a/b")) == json("a/b"));
#ifdef __cpp_lib_char8_t
CHECK(json(std::u8string(u8"a/b")) == json("a/b"));
#endif
}
#endif
}