Compare commits

..
Author SHA1 Message Date
Niels Lohmann e46a982be4 json_view: remove checks that invariants make dead
Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by the code around it:

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

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

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 10:05:55 +01:00
Niels Lohmann 73b0a3aba3 Drop the null_result parameter JSON_IMPLEMENT_OPERATOR no longer uses
Without the same-type scalar cases, the macro body has no use for it, and
JSON_IMPLEMENT_SCALAR_CASES is now used only by JSON_IMPLEMENT_SCALAR_FAST.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 10:05:55 +01:00
Niels Lohmann 7196bc5b22 iter_impl: keep the type-dependent initialization in the constructor
The check is not needed for correctness (m_it is value-initialized), but
without it GCC 13 generates slower iteration loops: a recursive walk over
a parsed document takes 0.5-2.7 % longer (callgrind: +1.9 % instructions
on twitter.json, +0.95 % on canada.json). Clang generates identical code
either way. See the measurements in #6005.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 10:05:54 +01:00
Niels Lohmann 2ac0851157 basic_json, iterators, serializer: remove checks that invariants make dead
Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by the code around it:

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5NasAgbDGG28MqXjQvLqp
Signed-off-by: Claude <noreply@anthropic.com>
2026-11-02 05:59:57 +00:00
Niels Lohmann 056ba3b7c1 Tests: cover the remaining lines of the library headers
Add tests for the lines of include/nlohmann that were not covered: the
binary readers and writers, the lexer and locale paths, the json_view
modules, the custom number, key and array types, json_pointer, json_patch,
diff and the iterators.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 22:32:42 +01:00
Niels Lohmann d7b9472173 Remove unreachable code found by the coverage analysis
Remove code that no input can reach, and mark the lines that cannot be
covered with LCOV_EXCL_LINE:

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

No behavior changes for any input that reaches these functions.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 22:32:41 +01:00
Niels Lohmann 04a5accfb9 CI: measure line coverage without lcov's unexecuted-block adjustment
Drop geninfo_unexecuted_blocks=1 from the lcov capture of ci_test_coverage:
it reset every line of an inlined or templated function to 0 as soon as one
copy had an unexecuted block, which understated the line coverage by about
three points.

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

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 22:32:40 +01:00
Niels Lohmann ddc34f546a json_view: exceptions name the path and the bytes of the value (#5974)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:25:26 +01:00
Niels Lohmann f022e3c59c Binary readers and writers: faster UTF-8 checks of strings (#5982)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:23:27 +01:00
Niels Lohmann 9742edc294 BSON writer: write to_bson(j) in one pass, with sizes written afterwards (#5979)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:23:12 +01:00
Niels Lohmann 517444b504 accept() checks strings without copying them (#5978)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:22:58 +01:00
Niels Lohmann fb515ddc34 UBJSON/BJData readers: read optimized arrays of numbers in one loop (#5981)
* UBJSON/BJData readers: read optimized arrays of numbers in one loop

An array with a type marker for a fixed-size number ($U, $i, $I, $l, $L,
$d, $D, and BJData's $u, $m, $M), including the data of a BJData ndarray,
gives its elements no markers. When the input is contiguous, read the
elements that are in the input in one loop - load, byte swap for the
format's byte order, emit - instead of one trip through
parse_ubjson_internal and get_ubjson_value per element. Elements the
input ends before, other types, and non-contiguous inputs are read as
before.

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

* UBJSON/BJData tests: assign the parsed value instead of casting it to void

GCC reports -Werror=unused-result for static_cast<void>(json::from_ubjson(...))
and json::from_bjdata(...): a cast to void does not silence warn_unused_result
on GCC. Assign to a json, as the other tests do.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:22:33 +01:00
Niels Lohmann edf8d17601 MessagePack reader: read runs of scalar array elements in one loop (#5980)
* MessagePack reader: read runs of scalar array elements in one loop

As for CBOR: when an array opens and the input is contiguous, read the
integers, floats, booleans, and nils that follow in a tight loop instead
of going through parse_msgpack_internal and parse_msgpack_value for every
element. The loop stops at any other kind of value and when fewer than 9
bytes are left; those values are read by the general path as before.
Non-contiguous inputs are not affected.

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

* MessagePack reader: enter the scalar run only when a scalar follows

As for CBOR: enter_msgpack_array tests the first byte and the remaining length
inline and calls the out-of-line run only for a scalar, so arrays of maps or
strings no longer pay for a call that reads nothing; the run's loop condition
moves to the end (do-while).

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

* MessagePack reader: mark the initial accepted in the scalar run as a false-positive dead store

ci_infer reports DEAD_STORE for 'bool accepted = true;' in the MessagePack
scalar run, as in the CBOR one, because every case assigns accepted before it
is read.

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

* MessagePack tests: assign the parsed value instead of casting it to void

GCC (ci_test_gcc) reports -Werror=unused-result for
static_cast<void>(json::from_msgpack(...)): a cast to void does not silence
warn_unused_result on GCC. Assign to a json, as the other tests in the file do.

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

* MessagePack reader: drop the scalar pre-check before the run

enter_msgpack_array now calls the run directly, and parse_msgpack_scalar_run
tests the remaining count and length at the top of a while loop instead of
relying on msgpack_scalar_follows(); its own switch already leaves on any
value that is not a scalar.

Instructions per repetition, new/old: SAX canada 0.946 (clang) and 0.973
(GCC 13), DOM read canada 0.991 / 0.992; no row more than 0.3 % higher.

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

* MessagePack reader: say why the initial accepted in the scalar run stays

As for CBOR: Infer's DEAD_STORE report is right, not a false positive. The
initializer is kept for clang-tidy's cppcoreguidelines-init-variables.

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

* MessagePack reader: give the scalar run the bulk tag instead of a wrapper

As for CBOR: parse_msgpack_scalar_run takes the bulk-scan tag itself and is
the function kept out of line, so parse_msgpack_array_start goes.

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

* MessagePack reader: start the scalar run's accepted at false

As for CBOR: Infer ignores dead stores of 0, so the suppression goes.

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

* MessagePack reader: let the scalar run find its array itself

As for CBOR: parse_msgpack_scalar_run reads container_stack.back() in its
bulk version, so the caller no longer evaluates it for input that is not
contiguous.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:22:32 +01:00
Niels Lohmann aecbef3949 Binary readers: swap the bytes of floats with a single byteswap (#5976)
* Binary readers: swap the bytes of floats with a single byteswap

byte_swap reversed float and double values byte by byte. GCC does not
merge that loop into one bswap, and C++23's std::byteswap accepts only
integers. Overloads for float and double now copy the bits into a
uint32_t/uint64_t, call detail::byteswap, and copy them back. This
applies to every binary reader that uses get_number.

In the CBOR reader, the single- and double-precision float cases call a
non-inlined helper. Without it, GCC stopped inlining the stream
adapter's get_character() into parse_cbor_internal, which made reading
from a std::istream about 0.5 % slower.

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

* CBOR reader: inline get_cbor_float() and always inline get() instead

The never-inlined get_cbor_float() costs a call for every float read
from input without bulk access: reading canada.json from CBOR through a
FILE* became 5.6 % slower on Apple clang in the benchmark suite. The
helper existed only because GCC 13 stopped inlining the stream adapter's
get_character() into parse_cbor_internal. Always inlining
binary_reader::get() fixes that as well.

callgrind, all binary formats on buffer, std::istringstream and FILE*
input (canada and citm_catalog): GCC 13 0.986-1.003, GCC 16 0.994-1.006,
Clang 23 identical. Apple clang suite: FromBinaryFile/cbor/canada 0.939,
geomean of the 53 binary reader rows 0.998.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:22:32 +01:00
Niels Lohmann 6c3ab966d3 CBOR reader: read runs of scalar array elements in one loop (#5975)
* CBOR reader: read runs of scalar array elements in one loop

When a definite-length CBOR array opens and the input is contiguous, read
the integers, floats, booleans, and nulls that follow in a tight loop
instead of going through parse_cbor_internal's container logic and
parse_cbor_value's dispatch for every element. The loop stops at any other
kind of value and when fewer than 9 bytes are left; those values are read
by the general path exactly as before. Non-contiguous inputs are not
affected (tag dispatch on the bulk-scan capability).

Also pass a member empty string as the number source to number_float
instead of constructing a std::string for every float.

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

* CBOR reader: address review of the scalar-run reader

- parse_cbor_scalar_run: set the size explicitly in every case that
  reads a value, instead of relying on a default that every other case
  overwrites.
- Remove cbor_scalar_follows(): parse_cbor_scalar_run already returns on
  the same conditions, and without the pre-check SAX canada needs another
  8.5 % fewer instructions with GCC 13 (0.915 against the previous
  version), with no row more than 0.5 % worse (Mac and GCC).
- Restore the comment on why parse_cbor_internal copies the container
  frame.

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

* CBOR reader: enter the scalar run only when a scalar follows

An array of containers or strings called the out-of-line run start once per
array only to leave it at once; on nested data (arrays of objects) that call
made reading slower than before the run reader. enter_cbor_array now tests the
first byte and the remaining length inline and calls the run only for a
scalar, so the run's loop condition moves to the end (do-while).

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

* CBOR reader: mark the initial accepted in the scalar run as a false-positive dead store

ci_infer reports DEAD_STORE for 'bool accepted = true;' in
parse_cbor_scalar_run because every case assigns accepted before it is read.

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

* CBOR reader: drop the scalar pre-check before the run

enter_cbor_array calls the run directly again, and parse_cbor_scalar_run
tests the remaining count and length at the top of a while loop; its own
switch already leaves on any value that is not a scalar. The MessagePack
reader is built the same way.

Instructions per repetition, new/old: clang SAX canada 0.983, read canada
0.993; GCC 13 read canada 0.997, the other rows at most 1.004.

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

* CBOR reader: say why the initial accepted in the scalar run stays

Infer's DEAD_STORE report is right, not a false positive: every case that
reaches the read assigns accepted. The initializer is kept for clang-tidy's
cppcoreguidelines-init-variables.

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

* CBOR reader: give the scalar run the bulk tag instead of a wrapper

parse_cbor_scalar_run takes the bulk-scan tag itself and is the function that
is kept out of line, so the one-line parse_cbor_array_start wrapper goes, as
the UBJSON typed run already does.

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

* CBOR reader: start the scalar run's accepted at false

Infer ignores dead stores of 0 (--liveness-ignored-constant defaults to "0"),
and false is 0 to it, so the suppression and its comment go.

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

* CBOR reader: let the scalar run find its array itself

parse_cbor_scalar_run reads container_stack.back() in its bulk version, so
the caller no longer evaluates it for input that is not contiguous.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:22:31 +01:00
Niels Lohmann 5337d841d4 Document and test the lifetime of string_view members decoded from a json_view (#5973)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:21:31 +01:00
Niels Lohmann 162eda2574 json_view: skip the digits of a fraction 16 bytes at a time (SSE2) (#5971)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:21:19 +01:00
Niels Lohmann 1a018577cc json_view: accept() checks the text without writing an index (#5970)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:21:06 +01:00
Niels Lohmann 76001c6b4c BSON reader: find C-string terminators 8 bytes at a time, skip array element names (#5968)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:20:54 +01:00
Niels Lohmann ec6986c918 Add natvis visualizers for json_document and json_view (#5967)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:20:41 +01:00
Niels Lohmann 3fd3e0fdaa json_view: dump and read canada.json-style floats faster (batched float decisions) (#5908)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-01 17:20:29 +01:00
Niels Lohmann 9c349b1d7c Write integers of 17 to 20 digits eight digits at a time in dump() (#5964)
#5634 made dump() write integers below 10^16 eight digits at a time; larger
ones still took the two-digits-per-division loop and count_digits(). Random
64-bit integers almost all have 17 to 20 digits (99.9 % of signed_ints.json
and unsigned_ints.json), so Dump/signed_ints and Dump/unsigned_ints gained
only 1.4x over 3.12.0, against 2.3x for small integers.

A 64-bit magnitude is now written as its first one to four digits and two
groups of eight. Wider magnitudes (__int128 and class types, #5805) keep the
loop: number types of at most 64 bits are selected by type, so no comparison
is reported as always true; for others the value is compared. The sign
handling of #1447/#5815 (remove_sign() into number_magnitude_t) is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-31 13:54:45 +01:00
Niels Lohmann 367963f6bb Test escaped object keys decoded through NLOHMANN_DEFINE_TYPE from a json_view (#5972)
A json_view or json_document decodes a macro-defined struct directly and looks
the members up by their decoded keys. Only plain keys were covered so far.

The new section decodes structs defined with NLOHMANN_DEFINE_TYPE_INTRUSIVE,
..._WITH_DEFAULT and ..._WITH_NAMES from documents whose keys contain escape
sequences, and compares the result with the conversion of the materialized
value. It also pins the difference for a duplicate key written once plain and
once escaped (the view takes the first member, the materialized value keeps
the last one), and that an escaped backslash is part of the decoded key.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-31 13:54:20 +01:00
Niels Lohmann 4fe670b5f4 json_view: decode chains of unicode escapes in a loop (#5969)
After the first \uXXXX escape of a string, slow_string() now decodes the
escapes that follow it directly, while the next six bytes are \u and four hex
digits that are not a surrogate, instead of going round its main loop for each
of them (that is how Python's json.dumps writes non-ASCII text by default).
A surrogate, a bad digit, or fewer than six bytes leave the position at the
backslash: the main loop handles them as before and reports the same error at
the same position.

twitterescaped.json (simdjson-data, 549 KB, 33,036 escapes), json_document::parse(),
A/B in one binary, 6 code layouts, time after / before:

- Apple M1 Max, Apple clang 21: 0.897
- x86-64 Haswell, GCC 16.2: 0.907
- x86-64 Haswell, Clang 23: 0.910

The other files of the benchmark set (twitter, citm_catalog, canada, gsoc-2018,
apache_builds, instruments, tree-pretty, github_events, random, update-center,
semanticscholar-corpus, twitter_timeline) stay within the noise of each machine.

New test: chains of every length from 1 to 40 of escapes that give one, two,
and three bytes of UTF-8, followed by the end of the string, text, escapes,
surrogates, bad digits, and cut in every place, in keys and in values, against
json::parse().

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-31 13:54:02 +01:00
Niels Lohmann 755bb16d7a Fix the red CI on develop: remove the duplicate is_editable in basic_json_view (#5966)
#5919 replaced the bool template parameter Editable of basic_json_view by
view_mode Mode and defined is_editable from Mode. #5890 was written before
that and added a second is_editable = Editable to the private section, which
no longer compiles ("duplicate member", "undeclared identifier Editable"). It
broke every job that builds the json_view tests (macOS, MSVC, clang-cl,
clang on Windows, the C++20 module job, CodeQL).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-30 16:48:31 +01:00
108 changed files with 22010 additions and 4132 deletions

No files matched your search

-9
View File
@@ -154,15 +154,6 @@ and each page's title (H1) and declaration by
[`docs/docset/generate_docset.py`](https://github.com/nlohmann/json/blob/develop/docs/docset/generate_docset.py); [`docs/docset/generate_docset.py`](https://github.com/nlohmann/json/blob/develop/docs/docset/generate_docset.py);
`make build` reports API pages that cannot be classified. `make build` reports API pages that cannot be classified.
A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files skips the
compiler workflows (Ubuntu, macOS, Windows); the documentation and license checks still run. See
[`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters) for the list of
skipped files and what to do when a compiler job starts reading one of them.
Most compiler jobs run only once a maintainer adds the label "🚀 ready to merge" to a pull request; until then, a
smaller set of jobs runs on every push. See
[How CI runs](https://json.nlohmann.me/community/quality_assurance/#how-ci-runs) for the sets and when they run.
### Amalgamate the source code ### Amalgamate the source code
The single-header files The single-header files
+6 -83
View File
@@ -7,60 +7,25 @@ on:
- master - master
- release/* - release/*
pull_request: pull_request:
# labeled: the "🚀 ready to merge" label starts the full set (see below)
types: [opened, synchronize, reopened, labeled]
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/macos.yml'
workflow_dispatch: workflow_dispatch:
# A run triggered by any other label gets a group of its own, so it does not
# cancel the run of the last push; its jobs are skipped.
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref || github.run_id }}${{ github.event.action == 'labeled' && github.event.label.name != '🚀 ready to merge' && format('-{0}', github.run_id) || '' }} group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
permissions: permissions:
contents: read contents: read
# The jobs are split into a PR set and a full set like in ubuntu.yml (see "How
# CI runs" in docs/mkdocs/docs/community/quality_assurance.md): the PR set runs
# on every push; the full set after the PR set passed, on pushes and on pull
# requests with the "🚀 ready to merge" label. tools/ci_jobs checks that every
# job belongs to exactly one set.
jobs: jobs:
###########################################################################
# PR set
###########################################################################
macos-15: macos-15:
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
runs-on: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md runs-on: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
strategy: strategy:
matrix: matrix:
xcode: ['16.0', '16.4', '26.3'] xcode: ['16.0', '16.1', '16.2', '16.3', '16.4', '26.0.1', '26.1.1', '26.2', '26.3']
env: env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
steps: &xcode_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -72,11 +37,10 @@ jobs:
run: cd build ; ctest -j 10 --output-on-failure run: cd build ; ctest -j 10 --output-on-failure
macos-26: macos-26:
if: *pr_set
runs-on: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md runs-on: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
strategy: strategy:
matrix: matrix:
xcode: ['26.6'] xcode: ['26.4.1', '26.5', '26.6']
env: env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
@@ -92,13 +56,12 @@ jobs:
run: cd build ; ctest -j 10 --output-on-failure run: cd build ; ctest -j 10 --output-on-failure
xcode_standards: xcode_standards:
if: *pr_set
runs-on: macos-latest runs-on: macos-latest
strategy: strategy:
matrix: matrix:
standard: [14] standard: [11, 14, 17, 20, 23, 26]
steps: &xcode_standards_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -110,7 +73,6 @@ jobs:
run: cd build ; ctest -j 10 --output-on-failure run: cd build ; ctest -j 10 --output-on-failure
swiftpm: swiftpm:
if: *pr_set
runs-on: macos-15 runs-on: macos-15
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -144,42 +106,3 @@ jobs:
EOF EOF
cd /tmp/json-swiftpm-consumer cd /tmp/json-swiftpm-consumer
swift build swift build
###########################################################################
# full set: starts after the PR set passed
###########################################################################
macos-15_full:
needs: &pr_set_jobs [macos-15, macos-26, xcode_standards, swiftpm]
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
runs-on: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
strategy:
matrix:
xcode: ['16.1', '16.2', '16.3', '26.0.1', '26.1.1', '26.2']
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
steps: *xcode_steps
macos-26_full:
needs: *pr_set_jobs
if: *full_set
runs-on: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
strategy:
matrix:
xcode: ['26.4.1', '26.5']
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
steps: *xcode_steps
xcode_standards_full:
needs: *pr_set_jobs
if: *full_set
runs-on: macos-latest
strategy:
matrix:
standard: [11, 17, 20, 23, 26]
steps: *xcode_standards_steps
-83
View File
@@ -1,83 +0,0 @@
name: Repository checks
# Checks that concern files the compiler workflows (ubuntu.yml, macos.yml,
# windows.yml) skip with their path filter: the documentation, the license
# information, and the CI configuration itself. This workflow has no path filter.
on:
push:
branches:
- develop
- master
- release/*
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
check_ci_configuration:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check the path filters of the compiler workflows
run: python3 tools/check_path_filters/check_path_filters.py
- name: Check the job sets of the compiler workflows and their documentation
run: |
python3 -m venv venv
venv/bin/pip install --quiet -r tools/ci_jobs/requirements.txt
venv/bin/python tools/ci_jobs/ci_jobs.py
ci_reuse_compliance:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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_reuse_compliance
ci_test_documentation:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
+262 -422
View File
@@ -7,62 +7,17 @@ on:
- master - master
- release/* - release/*
pull_request: pull_request:
# labeled: the "🚀 ready to merge" label starts the full set (see below)
types: [opened, synchronize, reopened, labeled]
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/ubuntu.yml'
schedule:
# the nightly set (see below), on develop
- cron: '0 2 * * *'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: read contents: read
# A run triggered by any other label gets a group of its own, so it does not
# cancel the run of the last push; its jobs are skipped.
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref || github.run_id }}${{ github.event.action == 'labeled' && github.event.label.name != '🚀 ready to merge' && format('-{0}', github.run_id) || '' }} group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# The jobs are split into three sets (see "How CI runs" in
# docs/mkdocs/docs/community/quality_assurance.md):
#
# - PR set: every pull request push and every push; chosen because these jobs
# caught 92 % of the failing commits in Sep/Oct 2026.
# - full set: after the PR set passed, on pushes and on pull requests with the
# "🚀 ready to merge" label; every remaining compiler, version and standard.
# - nightly set: slow tools and rare compilers, nightly on develop, on pushes
# to master and release/*, and when started by hand.
#
# A matrix whose entries belong to different sets is split into one job per
# set; the jobs share their steps through YAML anchors. tools/ci_jobs checks
# that every job belongs to exactly one set.
jobs: jobs:
###########################################################################
# PR set
###########################################################################
ci_test_gcc: ci_test_gcc:
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: gcc:latest container: gcc:latest
steps: steps:
@@ -77,7 +32,6 @@ jobs:
run: cmake --build build --target ci_test_gcc run: cmake --build build --target ci_test_gcc
ci_meson_install: ci_meson_install:
if: *pr_set
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -122,342 +76,7 @@ jobs:
cmake -S tests/cmake_import/project -B build-import-split -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install-split cmake -S tests/cmake_import/project -B build-import-split -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install-split
cmake --build build-import-split cmake --build build-import-split
ci_static_analysis_ubuntu:
if: *pr_set
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_cpplint]
steps: &static_analysis_ubuntu_steps
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Install Valgrind
if: matrix.target == 'ci_test_valgrind'
run: sudo apt-get update ; sudo apt-get install -y valgrind
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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 ${{ matrix.target }}
ci_static_analysis_clang:
if: *pr_set
runs-on: ubuntu-latest
container: silkeh/clang:22
strategy:
matrix:
target: [ci_test_clang, ci_clang_tidy, ci_test_clang_sanitizer, ci_single_binaries]
steps: &static_analysis_clang_steps
- name: Install git, clang-tools, iwyu (ci_single_binaries), and unzip
run: apt-get update ; apt-get install -y git clang-tools iwyu unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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 ${{ matrix.target }}
ci_cmake_options:
if: *pr_set
runs-on: ubuntu-latest
container: ubuntu:24.04
strategy:
matrix:
target: [ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noglobaludls]
steps: &cmake_options_steps
- name: Install build-essential
run: apt-get update ; apt-get install -y build-essential unzip wget git
# the CMake 3.5.0 release binary that ci_cmake_flags downloads links
# against libidn.so.11, which Ubuntu 24.04 no longer ships
- name: Install libidn11 for CMake 3.5.0
if: matrix.target == 'ci_cmake_flags'
run: |
wget -q http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb
dpkg -i libidn11_1.33-2.2ubuntu2_amd64.deb
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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 ${{ matrix.target }}
ci_test_compilers_gcc_old:
if: *pr_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['4.8']
# official gcc:4.8/4.9/5/6 images fail to check out code (too old for
# actions/checkout); install the old compilers on top of official ubuntu:20.04
# instead, mirroring what the (now retired) custom json-ci image did.
container: ubuntu:20.04
steps: &compilers_gcc_old_steps
- name: Install g++-${{ matrix.compiler }}
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends software-properties-common ca-certificates gnupg make git
# add-apt-repository resolves the PPA through the Launchpad API,
# which intermittently times out or fails the team lookup (the plain
# "deb ..." sources below never hit Launchpad and never flake).
# Retry with backoff so a transient Launchpad blip does not fail CI.
for attempt in 1 2 3 4 5; do
add-apt-repository -y ppa:ubuntu-toolchain-r/test && break
echo "::warning::add-apt-repository ppa:ubuntu-toolchain-r/test failed (attempt ${attempt}/5); retrying"
sleep $((attempt * 10))
done
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates universe"
apt-get update
apt-get install -y --no-install-recommends g++-${{ matrix.compiler }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
ci_test_compilers_clang:
if: *pr_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['3.4']
container: silkeh/clang:${{ matrix.compiler }}
steps: &compilers_clang_steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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' }}
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
ci_test_standards_gcc:
if: *pr_set
runs-on: ubuntu-latest
container: gcc:latest
strategy:
matrix:
standard: [26]
steps: &standards_gcc_steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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_test_gcc_cxx${{ matrix.standard }}
ci_test_standards_clang:
if: *pr_set
runs-on: ubuntu-latest
container: silkeh/clang:latest
strategy:
matrix:
standard: [26]
stdlib: [libcxx]
steps: &standards_clang_steps
- name: Install git and unzip
run: apt-get update ; apt-get install -y git unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build with libc++
run: cmake --build build --target ci_test_clang_libcxx_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libcxx' }}
- name: Build with libstdc++
run: cmake --build build --target ci_test_clang_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libstdcxx' }}
ci_emscripten:
if: *pr_set
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Install emscripten
uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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
run: cmake --build build
###########################################################################
# full set: starts after the PR set passed
###########################################################################
ci_static_analysis_ubuntu_full:
needs: &pr_set_jobs [ci_test_gcc, ci_meson_install, ci_static_analysis_ubuntu, ci_static_analysis_clang, ci_cmake_options, ci_test_compilers_gcc_old, ci_test_compilers_clang, ci_test_standards_gcc, ci_test_standards_clang, ci_emscripten]
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_single_header, ci_cppcheck, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata]
steps: *static_analysis_ubuntu_steps
ci_static_analysis_clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: silkeh/clang:22
strategy:
matrix:
target: [ci_clang_analyze]
steps: *static_analysis_clang_steps
ci_cmake_options_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: ubuntu:24.04
strategy:
matrix:
target: [ci_cmake_flags, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local]
steps: *cmake_options_steps
ci_test_compilers_gcc_old_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['4.9', '5', '6']
container: ubuntu:20.04
steps: *compilers_gcc_old_steps
ci_test_compilers_gcc_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
strategy:
matrix:
# older GCC docker images (4, 5, 6) fail to check out code
compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', '16', 'latest']
container: gcc:${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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_test_compiler_default
ci_test_compilers_clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
container: silkeh/clang:${{ matrix.compiler }}
steps: *compilers_clang_steps
ci_test_standards_gcc_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: gcc:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23]
steps: *standards_gcc_steps
ci_test_standards_clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: ubuntu-latest
container: silkeh/clang:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23, 26]
stdlib: [libcxx, libstdcxx]
exclude:
# in the PR set
- standard: 26
stdlib: libcxx
steps: *standards_clang_steps
ci_module_cpp20:
needs: *pr_set_jobs
if: *full_set
strategy:
matrix:
container: ['gcc:latest', 'silkeh/clang:latest']
runs-on: ubuntu-latest
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The module test uses `import std;`, which needs CMake's experimental
# 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@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
with:
cmakeVersion: 4.3.4
# Clang: the std library module is provided by libc++ (the image's libstdc++
# ships none), and the image's libc++ module manifest has a broken relative
# path — repoint it at the real module sources.
- name: Use libc++ and fix its module manifest path (Clang)
if: matrix.container == 'silkeh/clang:latest'
run: |
echo "CXXFLAGS=-stdlib=libc++" >> "$GITHUB_ENV"
mkdir -p /usr/lib/share && ln -sf /usr/lib/llvm-*/share/libc++ /usr/lib/share/libc++
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_module_cpp20
###########################################################################
# nightly set
###########################################################################
ci_infer: ci_infer:
if: &nightly_set ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref != 'refs/heads/develop') }}
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: Harden Runner - name: Harden Runner
@@ -486,16 +105,76 @@ jobs:
path: ${{ github.workspace }}/build/build_infer/infer-out/report.txt path: ${{ github.workspace }}/build/build_infer/infer-out/report.txt
if-no-files-found: ignore if-no-files-found: ignore
ci_static_analysis_ubuntu_nightly: ci_static_analysis_ubuntu:
if: *nightly_set
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
strategy: strategy:
matrix: matrix:
target: [ci_test_valgrind] 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: *static_analysis_ubuntu_steps steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Install Valgrind
if: matrix.target == 'ci_test_valgrind'
run: sudo apt-get update ; sudo apt-get install -y valgrind
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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 ${{ matrix.target }}
ci_static_analysis_clang:
runs-on: ubuntu-latest
container: silkeh/clang:22
strategy:
matrix:
target: [ci_test_clang, ci_clang_tidy, ci_test_clang_sanitizer, ci_clang_analyze, ci_single_binaries]
steps:
- name: Install git, clang-tools, iwyu (ci_single_binaries), and unzip
run: apt-get update ; apt-get install -y git clang-tools iwyu unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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 ${{ matrix.target }}
ci_cmake_options:
runs-on: ubuntu-latest
container: ubuntu:24.04
strategy:
matrix:
target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local]
steps:
- name: Install build-essential
run: apt-get update ; apt-get install -y build-essential unzip wget git
# the CMake 3.5.0 release binary that ci_cmake_flags downloads links
# against libidn.so.11, which Ubuntu 24.04 no longer ships
- name: Install libidn11 for CMake 3.5.0
if: matrix.target == 'ci_cmake_flags'
run: |
wget -q http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb
dpkg -i libidn11_1.33-2.2ubuntu2_amd64.deb
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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 ${{ matrix.target }}
ci_test_coverage: ci_test_coverage:
if: *nightly_set
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
steps: steps:
- name: Harden Runner - name: Harden Runner
@@ -529,8 +208,128 @@ jobs:
path-to-lcov: ${{ github.workspace }}/build/json.info.filtered.noexcept path-to-lcov: ${{ github.workspace }}/build/json.info.filtered.noexcept
fail-on-error: false fail-on-error: false
ci_test_compilers_gcc_old:
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['4.8', '4.9', '5', '6']
# official gcc:4.8/4.9/5/6 images fail to check out code (too old for
# actions/checkout); install the old compilers on top of official ubuntu:20.04
# instead, mirroring what the (now retired) custom json-ci image did.
container: ubuntu:20.04
steps:
- name: Install g++-${{ matrix.compiler }}
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends software-properties-common ca-certificates gnupg make git
# add-apt-repository resolves the PPA through the Launchpad API,
# which intermittently times out or fails the team lookup (the plain
# "deb ..." sources below never hit Launchpad and never flake).
# Retry with backoff so a transient Launchpad blip does not fail CI.
for attempt in 1 2 3 4 5; do
add-apt-repository -y ppa:ubuntu-toolchain-r/test && break
echo "::warning::add-apt-repository ppa:ubuntu-toolchain-r/test failed (attempt ${attempt}/5); retrying"
sleep $((attempt * 10))
done
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ bionic universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial universe"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates main"
apt-add-repository -y "deb http://archive.ubuntu.com/ubuntu/ xenial-updates universe"
apt-get update
apt-get install -y --no-install-recommends g++-${{ matrix.compiler }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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
run: cmake --build build --target ci_test_compiler_g++-${{ matrix.compiler }}
ci_test_compilers_gcc:
runs-on: ubuntu-latest
strategy:
matrix:
# older GCC docker images (4, 5, 6) fail to check out code
compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', '16', 'latest']
container: gcc:${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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_test_compiler_default
ci_test_compilers_clang:
runs-on: ubuntu-latest
strategy:
matrix:
compiler: ['3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15-bullseye', '16', '17', '18', '19', '20', '21', '22', 'latest']
container: silkeh/clang:${{ matrix.compiler }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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' }}
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_test_compiler_default
ci_test_standards_gcc:
runs-on: ubuntu-latest
container: gcc:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23, 26]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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_test_gcc_cxx${{ matrix.standard }}
ci_test_standards_clang:
runs-on: ubuntu-latest
container: silkeh/clang:latest
strategy:
matrix:
standard: [11, 14, 17, 20, 23, 26]
stdlib: [libcxx, libstdcxx]
steps:
- name: Install git and unzip
run: apt-get update ; apt-get install -y git unzip
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
uses: lukka/get-cmake@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build with libc++
run: cmake --build build --target ci_test_clang_libcxx_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libcxx' }}
- name: Build with libstdc++
run: cmake --build build --target ci_test_clang_cxx${{ matrix.standard }}
if: ${{ matrix.stdlib == 'libstdcxx' }}
ci_cuda_example: ci_cuda_example:
if: *nightly_set
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -553,8 +352,37 @@ jobs:
- name: Build - name: Build
run: cmake --build build --target ci_cuda_example run: cmake --build build --target ci_cuda_example
ci_module_cpp20:
strategy:
matrix:
container: ['gcc:latest', 'silkeh/clang:latest']
runs-on: ubuntu-latest
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The module test uses `import std;`, which needs CMake's experimental
# 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@a89e43e22a96abafcd7a62db652a65ecd33052c9 # v4.4.4
with:
cmakeVersion: 4.3.4
# Clang: the std library module is provided by libc++ (the image's libstdc++
# ships none), and the image's libc++ module manifest has a broken relative
# path — repoint it at the real module sources.
- name: Use libc++ and fix its module manifest path (Clang)
if: matrix.container == 'silkeh/clang:latest'
run: |
echo "CXXFLAGS=-stdlib=libc++" >> "$GITHUB_ENV"
mkdir -p /usr/lib/share && ln -sf /usr/lib/llvm-*/share/libc++ /usr/lib/share/libc++
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ci_module_cpp20
ci_icpc: ci_icpc:
if: *nightly_set
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Intel discontinued the classic icc/icpc compiler in oneAPI 2024.0; this is # Intel discontinued the classic icc/icpc compiler in oneAPI 2024.0; this is
# Intel's own last officially published image that still includes it. # Intel's own last officially published image that still includes it.
@@ -574,7 +402,6 @@ jobs:
run: cmake --build build --target ci_icpc run: cmake --build build --target ci_icpc
ci_icpx: ci_icpx:
if: *nightly_set
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: intel/oneapi-hpckit:latest container: intel/oneapi-hpckit:latest
steps: steps:
@@ -589,7 +416,6 @@ jobs:
run: cmake --build build --target ci_icpx run: cmake --build build --target ci_icpx
ci_nvhpc: ci_nvhpc:
if: *nightly_set
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: nvcr.io/nvidia/nvhpc:25.5-devel-cuda12.9-ubuntu22.04 container: nvcr.io/nvidia/nvhpc:25.5-devel-cuda12.9-ubuntu22.04
steps: steps:
@@ -603,10 +429,54 @@ jobs:
- name: Build - name: Build
run: cmake --build build --target ci_nvhpc run: cmake --build build --target ci_nvhpc
ci_emscripten:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Install emscripten
uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Get latest CMake and ninja
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
run: cmake --build build
ci_test_documentation:
runs-on: ubuntu-26.04
strategy:
matrix:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: matrix.target == 'ci_test_documentation_mermaid'
with:
node-version: 24
- name: Run CMake
run: cmake -S . -B build -DJSON_CI=On
- name: Build
run: cmake --build build --target ${{ matrix.target }}
# Native Linux ARM64 runner with the latest GCC. The ubuntu-24.04-arm label is # Native Linux ARM64 runner with the latest GCC. The ubuntu-24.04-arm label is
# only available for public repositories. # only available for public repositories.
ci_test_arm64: ci_test_arm64:
if: *nightly_set
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
container: gcc:latest container: gcc:latest
steps: steps:
@@ -621,33 +491,3 @@ jobs:
run: cmake -S . -B build -DJSON_CI=On run: cmake -S . -B build -DJSON_CI=On
- name: Build - name: Build
run: cmake --build build --target ci_test_compiler_default run: cmake --build build --target ci_test_compiler_default
# A failure of a scheduled run opens an issue, or comments on the open one.
nightly_report:
needs: [ci_infer, ci_static_analysis_ubuntu_nightly, ci_test_coverage, ci_cuda_example, ci_icpc, ci_icpx, ci_nvhpc, ci_test_arm64]
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
runs-on: ubuntu-26.04
permissions:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit
- name: Open or update the nightly failure issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
WORKFLOW: ${{ github.workflow }}
SHA: ${{ github.sha }}
run: |
title="Nightly CI failure (${WORKFLOW})"
body="The nightly run of ${WORKFLOW} on ${SHA:0:10} failed: ${RUN_URL}"
number=$(gh issue list --state open --search "in:title \"${title}\"" --json number,title --jq "map(select(.title == \"${title}\")) | .[0].number // empty")
if [ -n "$number" ]; then
gh issue comment "$number" --body "$body"
else
gh issue create --title "$title" --body "$body"
fi
+33 -215
View File
@@ -7,62 +7,23 @@ on:
- master - master
- release/* - release/*
pull_request: pull_request:
# labeled: the "🚀 ready to merge" label starts the full set (see below)
types: [opened, synchronize, reopened, labeled]
# Skip pull requests that change only files no job here reads. The list is checked by
# tools/check_path_filters/check_path_filters.py (repository_checks.yml); change both together.
paths:
- '**'
- '!docs/**'
- '!**.md'
- '!.github/*'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/external_ci/**'
- '!.github/workflows/**'
- '!.reuse/**'
- '!LICENSES/**'
- '!CITATION.cff'
- '!REUSE.toml'
# read by tools/check_build_options in the ci_meson_install job (ubuntu.yml)
- 'docs/mkdocs/docs/integration/package_managers.md'
# built by the swiftpm job (macos.yml)
- 'docs/mkdocs/docs/integration/swift/**'
- '.github/workflows/windows.yml'
schedule:
# the nightly set (see below), on develop
- cron: '0 2 * * *'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: read contents: read
# A run triggered by any other label gets a group of its own, so it does not
# cancel the run of the last push; its jobs are skipped.
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref || github.run_id }}${{ github.event.action == 'labeled' && github.event.label.name != '🚀 ready to merge' && format('-{0}', github.run_id) || '' }} group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# The jobs are split into three sets like in ubuntu.yml (see "How CI runs" in
# docs/mkdocs/docs/community/quality_assurance.md): the PR set runs on every
# push; the full set after the PR set passed, on pushes and on pull requests
# with the "🚀 ready to merge" label; the nightly set nightly on develop, on
# pushes to master and release/*, and when started by hand. tools/ci_jobs
# checks that every job belongs to exactly one set.
jobs: jobs:
###########################################################################
# PR set
###########################################################################
mingw: mingw:
if: &pr_set ${{ github.event_name != 'schedule' && (github.event.action != 'labeled' || github.event.label.name == '🚀 ready to merge') }}
runs-on: windows-2022 runs-on: windows-2022
strategy: strategy:
matrix: matrix:
architecture: [x86] architecture: [x64, x86]
steps: &mingw_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -79,26 +40,15 @@ jobs:
run: cd build ; ctest -j 10 -C Debug --output-on-failure run: cd build ; ctest -j 10 -C Debug --output-on-failure
msvc: msvc:
if: *pr_set
strategy: strategy:
matrix: matrix:
include: build_type: [Debug, Release]
- build_type: Debug architecture: [Win32, x64]
architecture: x64 std_version: [default, latest]
std_version: default
- build_type: Release
architecture: x64
std_version: default
- build_type: Debug
architecture: Win32
std_version: default
- build_type: Release
architecture: Win32
std_version: latest
runs-on: windows-2022 runs-on: windows-2022
steps: &msvc_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -127,20 +77,15 @@ jobs:
# Visual Studio 2026 (v145 toolset) on the windows-2025 image. The "Visual Studio # Visual Studio 2026 (v145 toolset) on the windows-2025 image. The "Visual Studio
# 18 2026" generator requires CMake 4.2+, so a recent CMake is fetched explicitly. # 18 2026" generator requires CMake 4.2+, so a recent CMake is fetched explicitly.
msvc-vs2026: msvc-vs2026:
if: *pr_set
strategy: strategy:
matrix: matrix:
include: build_type: [Debug, Release]
- build_type: Debug architecture: [Win32, x64]
architecture: x64 std_version: [default, latest]
std_version: latest
- build_type: Release
architecture: Win32
std_version: default
runs-on: windows-2025 runs-on: windows-2025
steps: &msvc_vs2026_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -172,14 +117,13 @@ jobs:
# Native ARM64 Windows runner with the MSVC ARM64 toolset. The windows-11-arm # Native ARM64 Windows runner with the MSVC ARM64 toolset. The windows-11-arm
# label is only available for public repositories. # label is only available for public repositories.
msvc-arm64: msvc-arm64:
if: *pr_set
strategy: strategy:
matrix: matrix:
build_type: [Debug] build_type: [Debug, Release]
runs-on: windows-11-arm runs-on: windows-11-arm
steps: &msvc_arm64_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -197,13 +141,12 @@ jobs:
run: cd build ; ctest -j 10 -C ${{ matrix.build_type }} --output-on-failure run: cd build ; ctest -j 10 -C ${{ matrix.build_type }} --output-on-failure
clang: clang:
if: *pr_set
runs-on: windows-2022 runs-on: windows-2022
strategy: strategy:
matrix: matrix:
version: [20.1.8] version: [11.0.1, 12.0.1, 13.0.1, 14.0.6, 15.0.7, 16.0.6, 18.1.8, 19.1.7, 20.1.8]
steps: &clang_steps steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -246,8 +189,24 @@ jobs:
- name: Test - name: Test
run: cd build ; ctest -j 10 -C Debug --output-on-failure run: cd build ; ctest -j 10 -C Debug --output-on-failure
clang-cl:
runs-on: windows-2022
strategy:
matrix:
architecture: [Win32, x64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run CMake
run: cmake -S . -B build -G "Visual Studio 17 2022" -A ${{ matrix.architecture }} -T ClangCL -DJSON_BuildTests=On
- name: Build
run: cmake --build build --config Debug --parallel 10
- name: Test
run: cd build ; ctest -j 10 -C Debug --output-on-failure
ci_module_cpp20: ci_module_cpp20:
if: *pr_set
runs-on: windows-2022 runs-on: windows-2022
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -266,145 +225,4 @@ jobs:
run: cmake -S . -B build -G "Visual Studio 17 2022" -DJSON_CI=ON -DCMAKE_CXX_FLAGS="/permissive- /std:c++latest /utf-8 /EHsc /W4 /WX" run: cmake -S . -B build -G "Visual Studio 17 2022" -DJSON_CI=ON -DCMAKE_CXX_FLAGS="/permissive- /std:c++latest /utf-8 /EHsc /W4 /WX"
- name: Build - name: Build
run: cmake --build build --config Debug --target ci_module_cpp20 run: cmake --build build --config Debug --target ci_module_cpp20
###########################################################################
# full set: starts after the PR set passed
###########################################################################
msvc_full:
needs: &pr_set_jobs [mingw, msvc, msvc-vs2026, msvc-arm64, clang, ci_module_cpp20]
if: &full_set ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, '🚀 ready to merge') }}
strategy:
matrix:
build_type: [Debug, Release]
architecture: [Win32, x64]
std_version: [default, latest]
exclude:
# in the PR set
- build_type: Debug
architecture: x64
std_version: default
- build_type: Release
architecture: x64
std_version: default
- build_type: Debug
architecture: Win32
std_version: default
- build_type: Release
architecture: Win32
std_version: latest
runs-on: windows-2022
steps: *msvc_steps
# Visual Studio 2026 (v145 toolset) on the windows-2025 image. The "Visual Studio
# 18 2026" generator requires CMake 4.2+, so a recent CMake is fetched explicitly.
msvc-vs2026_full:
needs: *pr_set_jobs
if: *full_set
strategy:
matrix:
build_type: [Debug, Release]
architecture: [Win32, x64]
std_version: [default, latest]
exclude:
# in the PR set
- build_type: Debug
architecture: x64
std_version: latest
- build_type: Release
architecture: Win32
std_version: default
runs-on: windows-2025
steps: *msvc_vs2026_steps
# Native ARM64 Windows runner with the MSVC ARM64 toolset. The windows-11-arm
# label is only available for public repositories.
msvc-arm64_full:
needs: *pr_set_jobs
if: *full_set
strategy:
matrix:
build_type: [Release]
runs-on: windows-11-arm
steps: *msvc_arm64_steps
clang_full:
needs: *pr_set_jobs
if: *full_set
runs-on: windows-2022
strategy:
matrix:
version: [11.0.1, 12.0.1, 13.0.1, 14.0.6, 15.0.7, 16.0.6, 18.1.8, 19.1.7]
steps: *clang_steps
clang-cl_full:
needs: *pr_set_jobs
if: *full_set
runs-on: windows-2022
strategy:
matrix:
architecture: [Win32, x64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run CMake
run: cmake -S . -B build -G "Visual Studio 17 2022" -A ${{ matrix.architecture }} -T ClangCL -DJSON_BuildTests=On
- name: Build
run: cmake --build build --config Debug --parallel 10
- name: Test
run: cd build ; ctest -j 10 -C Debug --output-on-failure
###########################################################################
# nightly set
###########################################################################
mingw_nightly:
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref != 'refs/heads/develop') }}
runs-on: windows-2022
strategy:
matrix:
architecture: [x64]
steps: *mingw_steps
# A failure of a scheduled run opens an issue, or comments on the open one.
nightly_report:
needs: [mingw_nightly]
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
runs-on: ubuntu-26.04
permissions:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- name: Open or update the nightly failure issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
WORKFLOW: ${{ github.workflow }}
SHA: ${{ github.sha }}
run: |
title="Nightly CI failure (${WORKFLOW})"
body="The nightly run of ${WORKFLOW} on ${SHA:0:10} failed: ${RUN_URL}"
number=$(gh issue list --state open --search "in:title \"${title}\"" --json number,title --jq "map(select(.title == \"${title}\")) | .[0].number // empty")
if [ -n "$number" ]; then
gh issue comment "$number" --body "$body"
else
gh issue create --title "$title" --body "$body"
fi
+3 -3
View File
@@ -348,20 +348,20 @@ add_custom_target(ci_test_no_thread_local
add_custom_target(ci_test_coverage add_custom_target(ci_test_coverage
COMMAND CXX=g++ ${CMAKE_COMMAND} COMMAND CXX=g++ ${CMAKE_COMMAND}
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="--coverage;-fprofile-arcs;-ftest-coverage" -DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="--coverage;-fprofile-arcs;-ftest-coverage;-fvar-tracking-assignments"
-DJSON_BuildTests=ON -DJSON_BuildTests=ON
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage
COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND CXX=g++ ${CMAKE_COMMAND} COMMAND CXX=g++ ${CMAKE_COMMAND}
-DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="-m32;--coverage;-fprofile-arcs;-ftest-coverage" -DCMAKE_BUILD_TYPE=Debug -GNinja -DCMAKE_CXX_FLAGS="-m32;--coverage;-fprofile-arcs;-ftest-coverage;-fvar-tracking-assignments"
-DJSON_BuildTests=ON -DJSON_32bitTest=ONLY -DJSON_BuildTests=ON -DJSON_32bitTest=ONLY
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage32 -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_coverage32
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage32 COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_coverage32
COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage32 && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure COMMAND cd ${PROJECT_BINARY_DIR}/build_coverage32 && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
COMMAND ${LCOV_TOOL} --directory . --capture --output-file json.info --rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1 --ignore-errors mismatch --ignore-errors unused --ignore-errors inconsistent COMMAND ${LCOV_TOOL} --directory . --capture --output-file json.info --rc branch_coverage=1 --ignore-errors mismatch --ignore-errors unused --ignore-errors inconsistent
COMMAND ${LCOV_TOOL} -e json.info ${SRC_FILES} --output-file json.info.filtered --rc branch_coverage=1 --ignore-errors unused --ignore-errors inconsistent COMMAND ${LCOV_TOOL} -e json.info ${SRC_FILES} --output-file json.info.filtered --rc branch_coverage=1 --ignore-errors unused --ignore-errors inconsistent
COMMAND ${CMAKE_SOURCE_DIR}/tests/thirdparty/imapdl/filterbr.py json.info.filtered > json.info.filtered.noexcept COMMAND ${CMAKE_SOURCE_DIR}/tests/thirdparty/imapdl/filterbr.py json.info.filtered > json.info.filtered.noexcept
COMMAND ${GENHTML_TOOL} --title "JSON for Modern C++" --legend --demangle-cpp --output-directory html --show-details --branch-coverage --ignore-errors inconsistent json.info.filtered.noexcept COMMAND ${GENHTML_TOOL} --title "JSON for Modern C++" --legend --demangle-cpp --output-directory html --show-details --branch-coverage --ignore-errors inconsistent json.info.filtered.noexcept
@@ -13,9 +13,15 @@ function never throws an exception for invalid input, and the returned `#!cpp bo
is returned. is returned.
The result is the same as that of `BasicJsonType::accept()` by design; the difference is how the check is made. This The result is the same as that of `BasicJsonType::accept()` by design; the difference is how the check is made. This
function runs the document's index builder (and discards the index), while `BasicJsonType::accept()` runs the SAX function runs the document's builder, which checks the text as [`parse()`](parse.md) does, while
parser with a handler that ignores every event. Use this one if you go on to parse the same input with `BasicJsonType::accept()` runs the SAX parser with a handler that ignores every event. Use this one if you go on to
[`parse()`](parse.md) anyway, so that both use the same code; either is fine for a plain yes/no answer. parse the same input with [`parse()`](parse.md) anyway, so that both use the same code; either is fine for a plain
yes/no answer.
When both `ignore_comments` and `ignore_trailing_commas` are `#!cpp false` (the default), the builder writes no index.
With either option set, it builds the index as [`parse()`](parse.md) does and discards it: the result is the same, only
the time differs. (Neither option needs the index; the variant that skips it exists only for the default options to
keep the code size down.)
## Template parameters ## Template parameters
+4 -3
View File
@@ -68,9 +68,10 @@ Strong guarantee: there are no changes to the view or the document it refers to.
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if `ptr` cannot be resolved - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if `ptr` cannot be resolved
because a reference token is used on a primitive value. because a reference token is used on a primitive value.
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with Pointer path of the value within the document, and with
`JSON_DIAGNOSTICS` enabled. [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
[`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
+3 -3
View File
@@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is
[null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object. [null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object.
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
`JSON_DIAGNOSTICS` enabled. its bytes in the source text (when [`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
@@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is
[null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object. [null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object.
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
`JSON_DIAGNOSTICS` enabled. its bytes in the source text (when [`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
+7 -8
View File
@@ -80,14 +80,13 @@ Strong guarantee: there are no changes to the view or the document it refers to.
- For every other `T`: whatever [`materialize().get<T>()`](../basic_json/get.md) throws — typically `type_error.302`, - For every other `T`: whatever [`materialize().get<T>()`](../basic_json/get.md) throws — typically `type_error.302`,
or whatever a user-provided `from_json()` throws. or whatever a user-provided `from_json()` throws.
None of the exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four The exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four bullets
bullets above) carry a above) carry the context of those of `BasicJsonType`: with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the
[`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no `BasicJsonType` value to point at. An JSON Pointer path of the value within the document, and with
exception thrown while converting through `materialize()` (the last bullet) is different: it is thrown by a real [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text. An exception
`BasicJsonType` value, so it **does** carry a `JSON_DIAGNOSTICS` path if `BasicJsonType` was built with it enabled. thrown while converting through `materialize()` (the last bullet) is thrown by the materialized `BasicJsonType` value
That path is relative to the root of the view's subtree, because `materialize()` builds only the subtree (with parent instead: its path is relative to the root of the view's subtree, because `materialize()` builds only the subtree
pointers): it is empty if the failing value is the subtree's root itself, and it never is the path within the whole (with parent pointers), so it is empty if the failing value is the subtree's root itself.
document.
## Complexity ## Complexity
@@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a string; example: Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a string; example:
`"type must be string, but is array"`. `"type must be string, but is array"`.
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
`JSON_DIAGNOSTICS` enabled. its bytes in the source text (when [`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
@@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a number; example: Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a number; example:
`"type must be number, but is string"`. `"type must be number, but is string"`.
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
`JSON_DIAGNOSTICS` enabled. its bytes in the source text (when [`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
@@ -73,9 +73,10 @@ Strong guarantee: there are no changes to the view or the document it refers to.
- [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in `ptr` is not a - [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in `ptr` is not a
number. number.
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with Pointer path of the value within the document, and with
`JSON_DIAGNOSTICS` enabled. [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
[`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
@@ -61,9 +61,10 @@ Strong guarantee: there are no changes to the view or the document it refers to.
`default_value` instead of throwing, exactly as [`BasicJsonType::value`](../basic_json/value.md) catches `default_value` instead of throwing, exactly as [`BasicJsonType::value`](../basic_json/value.md) catches
`out_of_range` and returns `default_value`. `out_of_range` and returns `default_value`.
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with Pointer path of the value within the document, and with
`JSON_DIAGNOSTICS` enabled. [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
[`materialize()`](materialize.md) sets them).
## Complexity ## Complexity
@@ -13,80 +13,6 @@ describe each of them.
<p class="nj-caption" markdown>The checks grouped by the stage at which they run, from push or pull request through the merge into develop to a release. The badge on each card says whether the check runs on every push or PR, on a schedule, or continuously.</p> <p class="nj-caption" markdown>The checks grouped by the stage at which they run, from push or pull request through the merge into develop to a release. The badge on each card says whether the check runs on every push or PR, on a schedule, or continuously.</p>
## How CI runs
Not every job runs for every change. The compiler jobs on Ubuntu, macOS, and Windows are split into three sets, so a
pull request gets fast feedback and the long jobs run when they matter:
| Set | When it runs | What it contains |
|-------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| PR set | every push to a pull request, and every push to `develop`, `master`, and `release/*` | GCC and Clang with all warnings, Clang-Tidy, sanitizers, IWYU, cpplint, the oldest and newest GCC and Clang, common CMake options, Meson, Emscripten, four Visual Studio 2022 and two Visual Studio 2026 configurations, MinGW, Clang on Windows, ARM64 Windows, five Xcode versions, Swift Package Manager; with the checks that always run, they would have caught at least 92 % of the failing commits of September and October 2026 |
| full set | after the PR set passed: on pull requests with the label :rocket: ready to merge, and on every push | every other compiler, version, C++ standard, and CMake option, the Clang Static Analyzer, Cppcheck, and the C++20 module tests |
| nightly set | every night on `develop`, on every push to `master` and `release/*`, and when started by hand | Infer, Valgrind, coverage, the Intel and NVIDIA compilers, CUDA, ARM64 Linux, and 64-bit MinGW; a failure opens an issue |
The documentation and license checks and the amalgamation check run on every push and pull request; AppVeyor (Visual
Studio 2015, 2017, and 2019) has its own schedule. A pull request that changes only the documentation, Markdown files, GitHub metadata, or license files
skips the compiler workflows (see [`tools/check_path_filters`](https://github.com/nlohmann/json/tree/develop/tools/check_path_filters)).
A maintainer adds the label :rocket: ready to merge when a pull request is ready; the full set then runs, and again on
every later push to that pull request. A pull request is merged only when the full set passed.
??? abstract "Jobs of the compiler workflows and their sets"
The table is generated by [`tools/ci_jobs`](https://github.com/nlohmann/json/tree/develop/tools/ci_jobs), which
also checks in CI that it is up to date and that every job belongs to exactly one set.
<!-- ci_jobs: begin -->
| Set | Workflow | Job | Configurations |
|-----|----------|-----|----------------|
| PR set | macos.yml | `macos-15` | 16.0; 16.4; 26.3 |
| PR set | macos.yml | `macos-26` | 26.6 |
| PR set | macos.yml | `xcode_standards` | 14 |
| PR set | macos.yml | `swiftpm` | - |
| PR set | ubuntu.yml | `ci_test_gcc` | - |
| PR set | ubuntu.yml | `ci_meson_install` | - |
| PR set | ubuntu.yml | `ci_static_analysis_ubuntu` | ci_cpplint |
| PR set | ubuntu.yml | `ci_static_analysis_clang` | ci_test_clang; ci_clang_tidy; ci_test_clang_sanitizer; ci_single_binaries |
| PR set | ubuntu.yml | `ci_cmake_options` | ci_test_diagnostics; ci_test_diagnostic_positions; ci_test_noexceptions; ci_test_noglobaludls |
| PR set | ubuntu.yml | `ci_test_compilers_gcc_old` | 4.8 |
| PR set | ubuntu.yml | `ci_test_compilers_clang` | 3.4 |
| PR set | ubuntu.yml | `ci_test_standards_gcc` | 26 |
| PR set | ubuntu.yml | `ci_test_standards_clang` | 26, libcxx |
| PR set | ubuntu.yml | `ci_emscripten` | - |
| PR set | windows.yml | `mingw` | x86 |
| PR set | windows.yml | `msvc` | Debug, x64, default; Release, x64, default; Debug, Win32, default; Release, Win32, latest |
| PR set | windows.yml | `msvc-vs2026` | Debug, x64, latest; Release, Win32, default |
| PR set | windows.yml | `msvc-arm64` | Debug |
| PR set | windows.yml | `clang` | 20.1.8 |
| PR set | windows.yml | `ci_module_cpp20` | - |
| full set | macos.yml | `macos-15_full` | 16.1; 16.2; 16.3; 26.0.1; 26.1.1; 26.2 |
| full set | macos.yml | `macos-26_full` | 26.4.1; 26.5 |
| full set | macos.yml | `xcode_standards_full` | 11; 17; 20; 23; 26 |
| full set | ubuntu.yml | `ci_static_analysis_ubuntu_full` | ci_test_single_header; ci_cppcheck; ci_reproducible_tests; ci_non_git_tests; ci_offline_testdata |
| full set | ubuntu.yml | `ci_static_analysis_clang_full` | ci_clang_analyze |
| full set | ubuntu.yml | `ci_cmake_options_full` | ci_cmake_flags; ci_test_noimplicitconversions; ci_test_legacycomparison; ci_test_disableenumserialization; ci_test_disabletuplereferenceconversion; ci_test_skiplibraryversioncheck; ci_test_simdutf; ci_test_strict_nul_handling; ci_test_delete_deprecated_functions; ci_test_no_thread_local |
| full set | ubuntu.yml | `ci_test_compilers_gcc_old_full` | 4.9; 5; 6 |
| full set | ubuntu.yml | `ci_test_compilers_gcc_full` | 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; latest |
| full set | ubuntu.yml | `ci_test_compilers_clang_full` | 3.5; 3.6; 3.7; 3.8; 3.9; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15-bullseye; 16; 17; 18; 19; 20; 21; 22; latest |
| full set | ubuntu.yml | `ci_test_standards_gcc_full` | 11; 14; 17; 20; 23 |
| full set | ubuntu.yml | `ci_test_standards_clang_full` | 11, libcxx; 11, libstdcxx; 14, libcxx; 14, libstdcxx; 17, libcxx; 17, libstdcxx; 20, libcxx; 20, libstdcxx; 23, libcxx; 23, libstdcxx; 26, libstdcxx |
| full set | ubuntu.yml | `ci_module_cpp20` | gcc:latest; silkeh/clang:latest |
| full set | windows.yml | `msvc_full` | Debug, Win32, latest; Debug, x64, latest; Release, Win32, default; Release, x64, latest |
| full set | windows.yml | `msvc-vs2026_full` | Debug, Win32, default; Debug, Win32, latest; Debug, x64, default; Release, Win32, latest; Release, x64, default; Release, x64, latest |
| full set | windows.yml | `msvc-arm64_full` | Release |
| full set | windows.yml | `clang_full` | 11.0.1; 12.0.1; 13.0.1; 14.0.6; 15.0.7; 16.0.6; 18.1.8; 19.1.7 |
| full set | windows.yml | `clang-cl_full` | Win32; x64 |
| nightly set | ubuntu.yml | `ci_infer` | - |
| nightly set | ubuntu.yml | `ci_static_analysis_ubuntu_nightly` | ci_test_valgrind |
| nightly set | ubuntu.yml | `ci_test_coverage` | - |
| nightly set | ubuntu.yml | `ci_cuda_example` | 11.8.0; 12.1.1; 12.6.3 |
| nightly set | ubuntu.yml | `ci_icpc` | - |
| nightly set | ubuntu.yml | `ci_icpx` | - |
| nightly set | ubuntu.yml | `ci_nvhpc` | - |
| nightly set | ubuntu.yml | `ci_test_arm64` | - |
| nightly set | windows.yml | `mingw_nightly` | x64 |
<!-- ci_jobs: end -->
## C++ language compliance and compiler compatibility ## C++ language compliance and compiler compatibility
!!! success "Requirement: Compiler support" !!! success "Requirement: Compiler support"
+47 -15
View File
@@ -134,9 +134,21 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
- **Macros.** [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) and - **Macros.** [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) and
[`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)/[`JSON_THROW_USER`](../api/macros/json_throw_user.md) [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)/[`JSON_THROW_USER`](../api/macros/json_throw_user.md)
behave the same way they do for `<nlohmann/json.hpp>`. These are the macros that change what the parser accepts or behave the same way they do for `<nlohmann/json.hpp>`. These are the macros that change what the parser accepts or
how it reports errors. The others either do not apply ([`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) how it reports errors. Two more macros add context to the exceptions of views, as they do for those of
needs parent pointers in `basic_json` values) or only affect the values `materialize()` produces, through `#!cpp basic_json`:
`BasicJsonType`.
- With [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md), an exception names the JSON Pointer path of the
value within the document.
- With [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), an exception names the bytes of
the value in the source text.
A view has neither parent pointers nor stored positions, so both are found from the document's index when the
exception is thrown. This takes time linear in the size of the document. One exception has neither:
`key()` of an iterator over an array or a primitive value
([`invalid_iterator.207`](../home/exceptions.md#jsonexceptioninvalid_iterator207)), because a view's iterator does
not know the value it iterates over.
The other macros only affect the values `materialize()` produces, through `BasicJsonType`.
## What is different ## What is different
@@ -167,10 +179,6 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
[`at`](../api/basic_json_view/at.md) throws for every missing value. [`at`](../api/basic_json_view/at.md) throws for every missing value.
- **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys) - **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys)
below. below.
- **No [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) path.** Exceptions thrown by `basic_json_view`'s own
element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the
view has no `basic_json` value to point at, so the exception is created without one, regardless of how
`BasicJsonType` was built.
- **Ordering comparisons are not provided** by `basic_json_view` — there is no `#!cpp operator<`. - **Ordering comparisons are not provided** by `basic_json_view` — there is no `#!cpp operator<`.
[`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are [`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are
provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when
@@ -253,6 +261,34 @@ Two conversions never copy at all:
Both results are only valid as long as the view — and, for a string with no escapes, the borrowed source text — is. Both results are only valid as long as the view — and, for a string with no escapes, the borrowed source text — is.
### `string_view` members
A struct whose member is a `#!cpp std::string_view` (C++17) can be decoded from a view with the
`NLOHMANN_DEFINE_TYPE_*` macros, and the member is not copied either: for a string without escape sequences it points
into the document's source text, and for a string with escape sequences into the document's buffer of decoded strings.
The member is valid as long as the document is alive and, for a document that
[borrows its source text](#ownership-and-lifetime) (as [`parse()`](../api/basic_json_document/parse.md) does for
an lvalue `#!cpp std::string`, a `#!cpp std::string_view`, or a C string), as long as that text is.
```cpp
struct record
{
std::string_view name;
int count;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(record, name, count)
};
const std::string text = R"({"name": "widget", "count": 3})";
const json_document doc = json_document::parse(text); // borrows text
const record r = doc.root().get<record>(); // r.name points into text, valid while text and doc are
```
!!! warning "Decoding into a `string_view` from a `json` value dangles at once"
The same struct also compiles with `#!cpp json::parse(text).get<record>()`, but there `name` points into the
temporary `json` value, which is destroyed at the end of the statement. Use a named
[`json_document`](../api/json_document.md), as above, to decode such a struct.
## Writing generic code ## Writing generic code
A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md): A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md):
@@ -329,14 +365,10 @@ Its body may only use what views and `basic_json` have in common: the type check
a view does not have; such a type is converted through `materialize()`. A `from_json()` for `json` only is a view does not have; such a type is converted through `materialize()`. A `from_json()` for `json` only is
converted through `materialize()` as well. converted through `materialize()` as well.
Two things differ when a `from_json()` reads the view: One thing differs when a `from_json()` reads the view: members looked up by name (`at()`, `value()`, ... and thus
the macros) find the **first** member with a repeated key, see [Duplicate keys](#duplicate-keys);
- **Duplicate keys.** Members looked up by name (`at()`, `value()`, ... and thus the macros) find the **first** member `#!cpp materialize().get<T>()` would see the last one. Containers built from an object, such as `#!cpp std::map`,
with a repeated key, see [Duplicate keys](#duplicate-keys); `#!cpp materialize().get<T>()` would see the last one. keep the **last** value either way, as [`parse()`](../api/basic_json/parse.md) does.
Containers built from an object, such as `#!cpp std::map`, keep the **last** value either way, as
[`parse()`](../api/basic_json/parse.md) does.
- **[`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md).** Exceptions thrown by the view carry no path to the
value.
`to_json()` is unaffected: it writes `basic_json` values, never views. `to_json()` is unaffected: it writes `basic_json` values, never views.
+8 -1
View File
@@ -10,6 +10,13 @@ at its root, a [Natvis](https://learn.microsoft.com/en-us/visualstudio/debugger/
file that gives `json`/`ordered_json` values a friendly, key/value debugger view instead of showing raw internal file that gives `json`/`ordered_json` values a friendly, key/value debugger view instead of showing raw internal
fields, when debugging with the MSVC debug engine (`cppvsdbg`) in Visual Studio or VS Code. fields, when debugging with the MSVC debug engine (`cppvsdbg`) in Visual Studio or VS Code.
The file also has visualizers for `json_document` and `json_view` (and their `ordered_json`/editable variants). They
show the value of the document or view, and its elements as an expandable list. Because a document is only a flat index
into the source text, the strings of the direct elements are shown, but the elements of nested arrays and objects are
shown as index nodes (their kind, size, and numbers; strings only by length). Arrays and objects that have been edited
in a `json_editable_document` are not expanded. The visualizers read the internal layout of the MSVC STL (`std::array`,
`std::unique_ptr`).
The checked-in visualizers cover namespace variants with zero, one, or two ABI tags. Configurations with three or more The checked-in visualizers cover namespace variants with zero, one, or two ABI tags. Configurations with three or more
ABI tags are not covered and may show raw internal fields instead. To generate a Natvis file for such a configuration: ABI tags are not covered and may show raw internal fields instead. To generate a Natvis file for such a configuration:
@@ -29,7 +36,7 @@ ABI tags are not covered and may show raw internal fields instead. To generate a
4. Use the generated file instead of `nlohmann_json.natvis`. 4. Use the generated file instead of `nlohmann_json.natvis`.
`--abi-tags` can be repeated for several configurations, and `--all` generates every combination (about 60 MB for 12 `--abi-tags` can be repeated for several configurations, and `--all` generates every combination (about 60 MB for 11
ABI tags, doubling with each further tag). See ABI tags, doubling with each further tag). See
[`tools/generate_natvis`](https://github.com/nlohmann/json/tree/develop/tools/generate_natvis) for all options. [`tools/generate_natvis`](https://github.com/nlohmann/json/tree/develop/tools/generate_natvis) for all options.
+160 -174
View File
@@ -1,31 +1,26 @@
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--wide" viewBox="0 0 760 862" role="img" aria-labelledby="nj-qa-title-w nj-qa-desc-w" xmlns="http://www.w3.org/2000/svg"> <svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--wide" viewBox="0 0 760 808" role="img" aria-labelledby="nj-qa-title-w nj-qa-desc-w" xmlns="http://www.w3.org/2000/svg">
<title id="nj-qa-title-w">Quality assurance pipeline</title> <title id="nj-qa-title-w">Quality assurance pipeline</title>
<desc id="nj-qa-desc-w">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C&#8203;UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (nightly): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Infer nightly; Dynamic analysis (every push / PR, nightly): runtime assertions Valgrind (Memcheck), nightly address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc> <desc id="nj-qa-desc-w">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
<defs><marker id="nj-qa-arrow-w" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs> <defs><marker id="nj-qa-arrow-w" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>
<rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="648" rx="12"/> <rect class="nj-exceptions-groupbox" x="12" y="70" width="736" height="594" rx="12"/>
<rect class="nj-exceptions-stage" x="265" y="8" width="230" height="34" rx="17"/> <rect class="nj-exceptions-stage" x="265" y="8" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="380" y="30">push or pull request</text> <text class="nj-exceptions-stagetext" x="380" y="30">push or pull request</text>
<line class="nj-exceptions-edge" x1="380" y1="42" x2="380" y2="68" marker-end="url(#nj-qa-arrow-w)"/> <line class="nj-exceptions-edge" x1="380" y1="42" x2="380" y2="68" marker-end="url(#nj-qa-arrow-w)"/>
<text class="nj-exceptions-group" x="26" y="94">checks, run in parallel on GitHub Actions and AppVeyor</text> <text class="nj-exceptions-group" x="26" y="94">checks, run in parallel on GitHub Actions and AppVeyor</text>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="110" width="228" height="238" rx="8"/> <rect class="nj-exceptions-box" x="26" y="110" width="228" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text> <text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="156.5" y="146" width="91.6" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="202.3" y="160">before merge</text>
<text class="nj-exceptions-line" x="40" y="188">50+ compilers: G&#8203;CC, Clang, M&#8203;SVC,</text> <text class="nj-exceptions-line" x="40" y="188">50+ compilers: G&#8203;CC, Clang, M&#8203;SVC,</text>
<text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text> <text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text>
<text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text> <text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text>
<text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text> <text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text>
<text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text> <text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text>
<text class="nj-exceptions-line" x="40" y="278">G&#8203;CC 300+ warnings</text> <text class="nj-exceptions-line" x="40" y="278">G&#8203;CC 300+ warnings</text>
<text class="nj-exceptions-line" x="40" y="296">a subset on every push and PR,</text>
<text class="nj-exceptions-line" x="40" y="314">all before merge; Intel, NVIDIA,</text>
<text class="nj-exceptions-line" x="40" y="332">C&#8203;UDA, and ARM64 nightly</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="266" y="110" width="228" height="238" rx="8"/> <rect class="nj-exceptions-box" x="266" y="110" width="228" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="135">Unit tests (doctest)</text> <text class="nj-exceptions-ctitle" x="280" y="135">Unit tests (doctest)</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="146" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="160">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="160">every push / PR</text>
@@ -37,218 +32,209 @@
<text class="nj-exceptions-line" x="280" y="278">and object types</text> <text class="nj-exceptions-line" x="280" y="278">and object types</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="506" y="110" width="228" height="238" rx="8"/> <rect class="nj-exceptions-box" x="506" y="110" width="228" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="135">Coverage</text> <text class="nj-exceptions-ctitle" x="520" y="135">Coverage</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="520" y="146" width="60.1" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="550.05" y="160">nightly</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="160">every push / PR</text>
<text class="nj-exceptions-line" x="520" y="188">unit tests cover all lines</text> <text class="nj-exceptions-line" x="520" y="188">unit tests cover all lines</text>
<text class="nj-exceptions-line" x="520" y="206">of the code base</text> <text class="nj-exceptions-line" x="520" y="206">of the code base</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="360" width="228" height="166" rx="8"/> <rect class="nj-exceptions-box" x="26" y="306" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="385">Static analysis</text> <text class="nj-exceptions-ctitle" x="40" y="331">Static analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="396" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="342" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="410">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="356">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="396" width="72.7" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="342" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="410">scheduled</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="356">scheduled</text>
<text class="nj-exceptions-line" x="40" y="438">Clang-Tidy, Cppcheck, Infer</text> <text class="nj-exceptions-line" x="40" y="384">Clang-Tidy, Cppcheck, Infer</text>
<text class="nj-exceptions-line" x="40" y="456">Clang Static Analyzer (52</text> <text class="nj-exceptions-line" x="40" y="402">Clang Static Analyzer (52</text>
<text class="nj-exceptions-line" x="40" y="474">checkers), I&#8203;WYU, Codacy</text> <text class="nj-exceptions-line" x="40" y="420">checkers), I&#8203;WYU, Codacy</text>
<text class="nj-exceptions-line" x="40" y="492">CodeQL, Semgrep, Flawfinder</text> <text class="nj-exceptions-line" x="40" y="438">CodeQL, Semgrep, Flawfinder</text>
<text class="nj-exceptions-line" x="40" y="510">(also scheduled); Infer nightly</text> <text class="nj-exceptions-line" x="40" y="456">(also scheduled)</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="266" y="360" width="228" height="166" rx="8"/> <rect class="nj-exceptions-box" x="266" y="306" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="385">Dynamic analysis</text> <text class="nj-exceptions-ctitle" x="280" y="331">Dynamic analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="396" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="342" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="410">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="356">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="396" width="60.1" height="20" rx="10"/> <text class="nj-exceptions-line" x="280" y="384">runtime assertions</text>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="426.55" y="410">nightly</text> <text class="nj-exceptions-line" x="280" y="402">Valgrind (Memcheck)</text>
<text class="nj-exceptions-line" x="280" y="438">runtime assertions</text> <text class="nj-exceptions-line" x="280" y="420">address and undefined behavior</text>
<text class="nj-exceptions-line" x="280" y="456">Valgrind (Memcheck), nightly</text> <text class="nj-exceptions-line" x="280" y="438">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="280" y="474">address and undefined behavior</text> <text class="nj-exceptions-line" x="280" y="456">nullability violations</text>
<text class="nj-exceptions-line" x="280" y="492">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="280" y="510">nullability violations</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="506" y="360" width="228" height="166" rx="8"/> <rect class="nj-exceptions-box" x="506" y="306" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="385">Fuzzing</text> <text class="nj-exceptions-ctitle" x="520" y="331">Fuzzing</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="396" width="66.4" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="342" width="66.4" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="553.2" y="410">every PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="553.2" y="356">every PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="592.4" y="396" width="79" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="592.4" y="342" width="79" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="631.9" y="410">continuous</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="631.9" y="356">continuous</text>
<text class="nj-exceptions-line" x="520" y="438">CIFuzz on every pull request</text> <text class="nj-exceptions-line" x="520" y="384">CIFuzz on every pull request</text>
<text class="nj-exceptions-line" x="520" y="456">OSS-Fuzz: billions of inputs</text> <text class="nj-exceptions-line" x="520" y="402">OSS-Fuzz: billions of inputs</text>
<text class="nj-exceptions-line" x="520" y="474">every crash fixed with a</text> <text class="nj-exceptions-line" x="520" y="420">every crash fixed with a</text>
<text class="nj-exceptions-line" x="520" y="492">reproducing unit test</text> <text class="nj-exceptions-line" x="520" y="438">reproducing unit test</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="538" width="228" height="166" rx="8"/> <rect class="nj-exceptions-box" x="26" y="484" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="563">Style</text> <text class="nj-exceptions-ctitle" x="40" y="509">Style</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="574" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="520" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="588">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="534">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="616">Artistic Style (astyle),</text> <text class="nj-exceptions-line" x="40" y="562">Artistic Style (astyle),</text>
<text class="nj-exceptions-line" x="40" y="634">enforced in the C&#8203;I</text> <text class="nj-exceptions-line" x="40" y="580">enforced in the C&#8203;I</text>
<text class="nj-exceptions-line" x="40" y="652">cpplint with 61 rules</text> <text class="nj-exceptions-line" x="40" y="598">cpplint with 61 rules</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="266" y="538" width="228" height="166" rx="8"/> <rect class="nj-exceptions-box" x="266" y="484" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="280" y="563">Docs and examples</text> <text class="nj-exceptions-ctitle" x="280" y="509">Docs and examples</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="574" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="280" y="520" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="588">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="335.25" y="534">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="574" width="72.7" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="396.5" y="520" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="432.85" y="588">scheduled</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="432.85" y="534">scheduled</text>
<text class="nj-exceptions-line" x="280" y="616">every documentation example is</text> <text class="nj-exceptions-line" x="280" y="562">every documentation example is</text>
<text class="nj-exceptions-line" x="280" y="634">tested; changed output fails</text> <text class="nj-exceptions-line" x="280" y="580">tested; changed output fails</text>
<text class="nj-exceptions-line" x="280" y="652">site built with warnings</text> <text class="nj-exceptions-line" x="280" y="598">site built with warnings</text>
<text class="nj-exceptions-line" x="280" y="670">as errors; links checked</text> <text class="nj-exceptions-line" x="280" y="616">as errors; links checked</text>
<text class="nj-exceptions-line" x="280" y="688">regularly (scheduled)</text> <text class="nj-exceptions-line" x="280" y="634">regularly (scheduled)</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="506" y="538" width="228" height="166" rx="8"/> <rect class="nj-exceptions-box" x="506" y="484" width="228" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="520" y="563">Integration, supply chain</text> <text class="nj-exceptions-ctitle" x="520" y="509">Integration, supply chain</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="574" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="520" y="520" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="588">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="575.25" y="534">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="636.5" y="574" width="72.7" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="636.5" y="520" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="672.85" y="588">scheduled</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="672.85" y="534">scheduled</text>
<text class="nj-exceptions-line" x="520" y="616">CMake 3.5, 3.31.6, and 4.0.0</text> <text class="nj-exceptions-line" x="520" y="562">CMake 3.5, 3.31.6, and 4.0.0</text>
<text class="nj-exceptions-line" x="520" y="634">Meson, Swift Package Manager</text> <text class="nj-exceptions-line" x="520" y="580">Meson, Swift Package Manager</text>
<text class="nj-exceptions-line" x="520" y="652">dependency review on every PR</text> <text class="nj-exceptions-line" x="520" y="598">dependency review on every PR</text>
<text class="nj-exceptions-line" x="520" y="670">Dependabot daily, OpenSSF</text> <text class="nj-exceptions-line" x="520" y="616">Dependabot daily, OpenSSF</text>
<text class="nj-exceptions-line" x="520" y="688">Scorecard, R&#8203;EUSE</text> <text class="nj-exceptions-line" x="520" y="634">Scorecard, R&#8203;EUSE</text>
</g> </g>
<line class="nj-exceptions-edge" x1="380" y1="718" x2="380" y2="744" marker-end="url(#nj-qa-arrow-w)"/> <line class="nj-exceptions-edge" x1="380" y1="664" x2="380" y2="690" marker-end="url(#nj-qa-arrow-w)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="265" y="746" width="230" height="34" rx="17"/> <rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="265" y="692" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="380" y="768">merge into develop</text> <text class="nj-exceptions-stagetext" x="380" y="714">merge into develop</text>
<line class="nj-exceptions-edge" x1="380" y1="780" x2="380" y2="806" marker-end="url(#nj-qa-arrow-w)"/> <line class="nj-exceptions-edge" x1="380" y1="726" x2="380" y2="752" marker-end="url(#nj-qa-arrow-w)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="230" y="808" width="300" height="46" rx="23"/> <rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="230" y="754" width="300" height="46" rx="23"/>
<text class="nj-exceptions-stagetext" x="380" y="830">release</text> <text class="nj-exceptions-stagetext" x="380" y="776">release</text>
<text class="nj-exceptions-stagesub" x="380" y="846">no open vulnerability alert</text> <text class="nj-exceptions-stagesub" x="380" y="792">no open vulnerability alert</text>
</svg> </svg>
<svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--narrow" viewBox="0 0 360 1840" role="img" aria-labelledby="nj-qa-title-n nj-qa-desc-n" xmlns="http://www.w3.org/2000/svg"> <svg class="nj-fig nj-exceptions-qa nj-exceptions-qa--narrow" viewBox="0 0 360 1786" role="img" aria-labelledby="nj-qa-title-n nj-qa-desc-n" xmlns="http://www.w3.org/2000/svg">
<title id="nj-qa-title-n">Quality assurance pipeline</title> <title id="nj-qa-title-n">Quality assurance pipeline</title>
<desc id="nj-qa-desc-n">A push or pull request starts these checks; the badges say when each runs: Build matrix (every push / PR, before merge): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings a subset on every push and PR, all before merge; Intel, NVIDIA, C&#8203;UDA, and ARM64 nightly; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (nightly): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Infer nightly; Dynamic analysis (every push / PR, nightly): runtime assertions Valgrind (Memcheck), nightly address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc> <desc id="nj-qa-desc-n">Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G&#8203;CC, Clang, M&#8203;SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G&#8203;CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A&#8203;PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I&#8203;WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C&#8203;I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R&#8203;EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.</desc>
<defs><marker id="nj-qa-arrow-n" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs> <defs><marker id="nj-qa-arrow-n" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>
<rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1626" rx="12"/> <rect class="nj-exceptions-groupbox" x="12" y="70" width="336" height="1572" rx="12"/>
<rect class="nj-exceptions-stage" x="65" y="8" width="230" height="34" rx="17"/> <rect class="nj-exceptions-stage" x="65" y="8" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="180" y="30">push or pull request</text> <text class="nj-exceptions-stagetext" x="180" y="30">push or pull request</text>
<line class="nj-exceptions-edge" x1="180" y1="42" x2="180" y2="68" marker-end="url(#nj-qa-arrow-n)"/> <line class="nj-exceptions-edge" x1="180" y1="42" x2="180" y2="68" marker-end="url(#nj-qa-arrow-n)"/>
<text class="nj-exceptions-group" x="26" y="94">checks, run in parallel</text> <text class="nj-exceptions-group" x="26" y="94">checks, run in parallel</text>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="110" width="308" height="238" rx="8"/> <rect class="nj-exceptions-box" x="26" y="110" width="308" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text> <text class="nj-exceptions-ctitle" x="40" y="135">Build matrix</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="146" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="160">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="156.5" y="146" width="91.6" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="202.3" y="160">before merge</text>
<text class="nj-exceptions-line" x="40" y="188">50+ compilers: G&#8203;CC, Clang, M&#8203;SVC,</text> <text class="nj-exceptions-line" x="40" y="188">50+ compilers: G&#8203;CC, Clang, M&#8203;SVC,</text>
<text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text> <text class="nj-exceptions-line" x="40" y="206">AppleClang, Intel, NVIDIA, ...</text>
<text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text> <text class="nj-exceptions-line" x="40" y="224">C++11, 14, 17, 20, 23, and 26</text>
<text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text> <text class="nj-exceptions-line" x="40" y="242">libc++ and libstdc++</text>
<text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text> <text class="nj-exceptions-line" x="40" y="260">warnings: Clang -Weverything,</text>
<text class="nj-exceptions-line" x="40" y="278">G&#8203;CC 300+ warnings</text> <text class="nj-exceptions-line" x="40" y="278">G&#8203;CC 300+ warnings</text>
<text class="nj-exceptions-line" x="40" y="296">a subset on every push and PR,</text>
<text class="nj-exceptions-line" x="40" y="314">all before merge; Intel, NVIDIA,</text>
<text class="nj-exceptions-line" x="40" y="332">C&#8203;UDA, and ARM64 nightly</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="360" width="308" height="184" rx="8"/> <rect class="nj-exceptions-box" x="26" y="306" width="308" height="184" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="385">Unit tests (doctest)</text> <text class="nj-exceptions-ctitle" x="40" y="331">Unit tests (doctest)</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="396" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="342" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="410">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="356">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="438">every public A&#8203;PI function</text> <text class="nj-exceptions-line" x="40" y="384">every public A&#8203;PI function</text>
<text class="nj-exceptions-line" x="40" y="456">every exception id and message</text> <text class="nj-exceptions-line" x="40" y="402">every exception id and message</text>
<text class="nj-exceptions-line" x="40" y="474">all Unicode code points</text> <text class="nj-exceptions-line" x="40" y="420">all Unicode code points</text>
<text class="nj-exceptions-line" x="40" y="492">single header and split headers</text> <text class="nj-exceptions-line" x="40" y="438">single header and split headers</text>
<text class="nj-exceptions-line" x="40" y="510">custom number, string, array,</text> <text class="nj-exceptions-line" x="40" y="456">custom number, string, array,</text>
<text class="nj-exceptions-line" x="40" y="528">and object types</text> <text class="nj-exceptions-line" x="40" y="474">and object types</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="556" width="308" height="112" rx="8"/> <rect class="nj-exceptions-box" x="26" y="502" width="308" height="112" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="581">Coverage</text> <text class="nj-exceptions-ctitle" x="40" y="527">Coverage</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="40" y="592" width="60.1" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="538" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="70.05" y="606">nightly</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="552">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="634">unit tests cover all lines</text> <text class="nj-exceptions-line" x="40" y="580">unit tests cover all lines</text>
<text class="nj-exceptions-line" x="40" y="652">of the code base</text> <text class="nj-exceptions-line" x="40" y="598">of the code base</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="680" width="308" height="166" rx="8"/> <rect class="nj-exceptions-box" x="26" y="626" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="705">Static analysis</text> <text class="nj-exceptions-ctitle" x="40" y="651">Static analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="716" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="662" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="730">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="676">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="716" width="72.7" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="662" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="730">scheduled</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="676">scheduled</text>
<text class="nj-exceptions-line" x="40" y="758">Clang-Tidy, Cppcheck, Infer</text> <text class="nj-exceptions-line" x="40" y="704">Clang-Tidy, Cppcheck, Infer</text>
<text class="nj-exceptions-line" x="40" y="776">Clang Static Analyzer (52</text> <text class="nj-exceptions-line" x="40" y="722">Clang Static Analyzer (52</text>
<text class="nj-exceptions-line" x="40" y="794">checkers), I&#8203;WYU, Codacy</text> <text class="nj-exceptions-line" x="40" y="740">checkers), I&#8203;WYU, Codacy</text>
<text class="nj-exceptions-line" x="40" y="812">CodeQL, Semgrep, Flawfinder</text> <text class="nj-exceptions-line" x="40" y="758">CodeQL, Semgrep, Flawfinder</text>
<text class="nj-exceptions-line" x="40" y="830">(also scheduled); Infer nightly</text> <text class="nj-exceptions-line" x="40" y="776">(also scheduled)</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="858" width="308" height="166" rx="8"/> <rect class="nj-exceptions-box" x="26" y="804" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="883">Dynamic analysis</text> <text class="nj-exceptions-ctitle" x="40" y="829">Dynamic analysis</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="894" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="840" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="908">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="854">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="894" width="60.1" height="20" rx="10"/> <text class="nj-exceptions-line" x="40" y="882">runtime assertions</text>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="186.55" y="908">nightly</text> <text class="nj-exceptions-line" x="40" y="900">Valgrind (Memcheck)</text>
<text class="nj-exceptions-line" x="40" y="936">runtime assertions</text> <text class="nj-exceptions-line" x="40" y="918">address and undefined behavior</text>
<text class="nj-exceptions-line" x="40" y="954">Valgrind (Memcheck), nightly</text> <text class="nj-exceptions-line" x="40" y="936">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="40" y="972">address and undefined behavior</text> <text class="nj-exceptions-line" x="40" y="954">nullability violations</text>
<text class="nj-exceptions-line" x="40" y="990">sanitizers, integer overflow,</text>
<text class="nj-exceptions-line" x="40" y="1008">nullability violations</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1036" width="308" height="148" rx="8"/> <rect class="nj-exceptions-box" x="26" y="982" width="308" height="148" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1061">Fuzzing</text> <text class="nj-exceptions-ctitle" x="40" y="1007">Fuzzing</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1072" width="66.4" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1018" width="66.4" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="73.2" y="1086">every PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="73.2" y="1032">every PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="112.4" y="1072" width="79" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--cont" x="112.4" y="1018" width="79" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="151.9" y="1086">continuous</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--cont" x="151.9" y="1032">continuous</text>
<text class="nj-exceptions-line" x="40" y="1114">CIFuzz on every pull request</text> <text class="nj-exceptions-line" x="40" y="1060">CIFuzz on every pull request</text>
<text class="nj-exceptions-line" x="40" y="1132">OSS-Fuzz: billions of inputs</text> <text class="nj-exceptions-line" x="40" y="1078">OSS-Fuzz: billions of inputs</text>
<text class="nj-exceptions-line" x="40" y="1150">every crash fixed with a</text> <text class="nj-exceptions-line" x="40" y="1096">every crash fixed with a</text>
<text class="nj-exceptions-line" x="40" y="1168">reproducing unit test</text> <text class="nj-exceptions-line" x="40" y="1114">reproducing unit test</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1196" width="308" height="130" rx="8"/> <rect class="nj-exceptions-box" x="26" y="1142" width="308" height="130" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1221">Style</text> <text class="nj-exceptions-ctitle" x="40" y="1167">Style</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1232" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1178" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1246">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1192">every push / PR</text>
<text class="nj-exceptions-line" x="40" y="1274">Artistic Style (astyle),</text> <text class="nj-exceptions-line" x="40" y="1220">Artistic Style (astyle),</text>
<text class="nj-exceptions-line" x="40" y="1292">enforced in the C&#8203;I</text> <text class="nj-exceptions-line" x="40" y="1238">enforced in the C&#8203;I</text>
<text class="nj-exceptions-line" x="40" y="1310">cpplint with 61 rules</text> <text class="nj-exceptions-line" x="40" y="1256">cpplint with 61 rules</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1338" width="308" height="166" rx="8"/> <rect class="nj-exceptions-box" x="26" y="1284" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1363">Docs and examples</text> <text class="nj-exceptions-ctitle" x="40" y="1309">Docs and examples</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1374" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1320" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1388">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1334">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1374" width="72.7" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1320" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1388">scheduled</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1334">scheduled</text>
<text class="nj-exceptions-line" x="40" y="1416">every documentation example is</text> <text class="nj-exceptions-line" x="40" y="1362">every documentation example is</text>
<text class="nj-exceptions-line" x="40" y="1434">tested; changed output fails</text> <text class="nj-exceptions-line" x="40" y="1380">tested; changed output fails</text>
<text class="nj-exceptions-line" x="40" y="1452">site built with warnings</text> <text class="nj-exceptions-line" x="40" y="1398">site built with warnings</text>
<text class="nj-exceptions-line" x="40" y="1470">as errors; links checked</text> <text class="nj-exceptions-line" x="40" y="1416">as errors; links checked</text>
<text class="nj-exceptions-line" x="40" y="1488">regularly (scheduled)</text> <text class="nj-exceptions-line" x="40" y="1434">regularly (scheduled)</text>
</g> </g>
<g class="nj-exceptions-card"> <g class="nj-exceptions-card">
<rect class="nj-exceptions-box" x="26" y="1516" width="308" height="166" rx="8"/> <rect class="nj-exceptions-box" x="26" y="1462" width="308" height="166" rx="8"/>
<text class="nj-exceptions-ctitle" x="40" y="1541">Integration, supply chain</text> <text class="nj-exceptions-ctitle" x="40" y="1487">Integration, supply chain</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1552" width="110.5" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--push" x="40" y="1498" width="110.5" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1566">every push / PR</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--push" x="95.25" y="1512">every push / PR</text>
<rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1552" width="72.7" height="20" rx="10"/> <rect class="nj-exceptions-badge nj-exceptions-badge--sched" x="156.5" y="1498" width="72.7" height="20" rx="10"/>
<text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1566">scheduled</text> <text class="nj-exceptions-badgetext nj-exceptions-badgetext--sched" x="192.85" y="1512">scheduled</text>
<text class="nj-exceptions-line" x="40" y="1594">CMake 3.5, 3.31.6, and 4.0.0</text> <text class="nj-exceptions-line" x="40" y="1540">CMake 3.5, 3.31.6, and 4.0.0</text>
<text class="nj-exceptions-line" x="40" y="1612">Meson, Swift Package Manager</text> <text class="nj-exceptions-line" x="40" y="1558">Meson, Swift Package Manager</text>
<text class="nj-exceptions-line" x="40" y="1630">dependency review on every PR</text> <text class="nj-exceptions-line" x="40" y="1576">dependency review on every PR</text>
<text class="nj-exceptions-line" x="40" y="1648">Dependabot daily, OpenSSF</text> <text class="nj-exceptions-line" x="40" y="1594">Dependabot daily, OpenSSF</text>
<text class="nj-exceptions-line" x="40" y="1666">Scorecard, R&#8203;EUSE</text> <text class="nj-exceptions-line" x="40" y="1612">Scorecard, R&#8203;EUSE</text>
</g> </g>
<line class="nj-exceptions-edge" x1="180" y1="1696" x2="180" y2="1722" marker-end="url(#nj-qa-arrow-n)"/> <line class="nj-exceptions-edge" x1="180" y1="1642" x2="180" y2="1668" marker-end="url(#nj-qa-arrow-n)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="65" y="1724" width="230" height="34" rx="17"/> <rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="65" y="1670" width="230" height="34" rx="17"/>
<text class="nj-exceptions-stagetext" x="180" y="1746">merge into develop</text> <text class="nj-exceptions-stagetext" x="180" y="1692">merge into develop</text>
<line class="nj-exceptions-edge" x1="180" y1="1758" x2="180" y2="1784" marker-end="url(#nj-qa-arrow-n)"/> <line class="nj-exceptions-edge" x1="180" y1="1704" x2="180" y2="1730" marker-end="url(#nj-qa-arrow-n)"/>
<rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="30" y="1786" width="300" height="46" rx="23"/> <rect class="nj-exceptions-stage nj-exceptions-stage--merge" x="30" y="1732" width="300" height="46" rx="23"/>
<text class="nj-exceptions-stagetext" x="180" y="1808">release</text> <text class="nj-exceptions-stagetext" x="180" y="1754">release</text>
<text class="nj-exceptions-stagesub" x="180" y="1824">no open vulnerability alert</text> <text class="nj-exceptions-stagesub" x="180" y="1770">no open vulnerability alert</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+6 -11
View File
@@ -22,24 +22,19 @@ P = "nj-exceptions-"
TRIGGERS = { TRIGGERS = {
"push": ("every push / PR", "push"), "push": ("every push / PR", "push"),
"pr": ("every PR", "push"), "pr": ("every PR", "push"),
"merge": ("before merge", "push"),
"nightly": ("nightly", "sched"),
"sched": ("scheduled", "sched"), "sched": ("scheduled", "sched"),
"cont": ("continuous", "cont"), "cont": ("continuous", "cont"),
} }
# title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing) # title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing)
LANES = [ LANES = [
("Build matrix", ["push", "merge"], [ ("Build matrix", ["push"], [
"50+ compilers: GCC, Clang, MSVC,", "50+ compilers: GCC, Clang, MSVC,",
"AppleClang, Intel, NVIDIA, ...", "AppleClang, Intel, NVIDIA, ...",
"C++11, 14, 17, 20, 23, and 26", "C++11, 14, 17, 20, 23, and 26",
"libc++ and libstdc++", "libc++ and libstdc++",
"warnings: Clang -Weverything,", "warnings: Clang -Weverything,",
"GCC 300+ warnings", "GCC 300+ warnings",
"a subset on every push and PR,",
"all before merge; Intel, NVIDIA,",
"CUDA, and ARM64 nightly",
]), ]),
("Unit tests (doctest)", ["push"], [ ("Unit tests (doctest)", ["push"], [
"every public API function", "every public API function",
@@ -49,7 +44,7 @@ LANES = [
"custom number, string, array,", "custom number, string, array,",
"and object types", "and object types",
]), ]),
("Coverage", ["nightly"], [ ("Coverage", ["push"], [
"unit tests cover all lines", "unit tests cover all lines",
"of the code base", "of the code base",
]), ]),
@@ -58,11 +53,11 @@ LANES = [
"Clang Static Analyzer (52", "Clang Static Analyzer (52",
"checkers), IWYU, Codacy", "checkers), IWYU, Codacy",
"CodeQL, Semgrep, Flawfinder", "CodeQL, Semgrep, Flawfinder",
"(also scheduled); Infer nightly", "(also scheduled)",
]), ]),
("Dynamic analysis", ["push", "nightly"], [ ("Dynamic analysis", ["push"], [
"runtime assertions", "runtime assertions",
"Valgrind (Memcheck), nightly", "Valgrind (Memcheck)",
"address and undefined behavior", "address and undefined behavior",
"sanitizers, integer overflow,", "sanitizers, integer overflow,",
"nullability violations", "nullability violations",
@@ -183,7 +178,7 @@ def drawing(W, cols, suffix):
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">' defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
'<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid) '<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix
desc = ("A push or pull request starts these checks; the badges say when each runs: " + desc = ("Every push or pull request starts these checks in parallel: " +
"; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) + "; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) +
". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.") ". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.")
out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">' out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">'
@@ -91,7 +91,7 @@ const typename BasicJsonType::binary_t& binary_value(const BasicJsonType& j)
} }
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0> template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) typename View::basic_json_type::binary_t binary_value(const View& /*unused*/) // LCOV_EXCL_LINE
{ {
return {}; // LCOV_EXCL_LINE return {}; // LCOV_EXCL_LINE
} }
+23 -1
View File
@@ -431,6 +431,28 @@ std::nullptr_t exception_context(const View& /*unused*/) noexcept
return nullptr; return nullptr;
} }
namespace view
{
// the path and bytes of the value of a view (defined in <nlohmann/detail/view/errors.hpp>)
template<typename View>
std::string diagnostics_of(const View& v);
} // namespace view
// what goes in front of the message of an exception about j: nothing for a
// basic_json value (exception_context() passes the value itself), the context
// basic_json would add (JSON_DIAGNOSTICS, JSON_DIAGNOSTIC_POSITIONS) for a view
template<typename BasicJsonType, enable_if_t<is_basic_json<BasicJsonType>::value, int> = 0>
const char* exception_prefix(const BasicJsonType& /*unused*/) noexcept
{
return "";
}
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
std::string exception_prefix(const View& v)
{
return view::diagnostics_of(v);
}
/*! /*!
@brief throws because @a j does not have the type a conversion expects @brief throws because @a j does not have the type a conversion expects
@param[in] expected the expected type(s), e.g. "array" or "binary or array" @param[in] expected the expected type(s), e.g. "array" or "binary or array"
@@ -442,7 +464,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha
{ {
static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(j); static_cast<void>(j);
JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j))); JSON_THROW(type_error::create(exception_id::type_mismatch, concat(exception_prefix(j), "type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
} }
/*! /*!
+498 -46
View File
@@ -22,6 +22,7 @@
#include <bit> //byteswap #include <bit> //byteswap
#endif #endif
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/exceptions.hpp> #include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/input/input_adapters.hpp> #include <nlohmann/detail/input/input_adapters.hpp>
#include <nlohmann/detail/input/json_sax.hpp> #include <nlohmann/detail/input/json_sax.hpp>
@@ -465,15 +466,17 @@ class binary_reader
} }
const std::size_t element_type_parse_position = chars_read; const std::size_t element_type_parse_position = chars_read;
// an array's elements are named "0", "1", ... in the wire format,
// and those names are not passed on: they are skipped unless
// error_handler_t::strict must reject ill-formed UTF-8 in them
const bool is_object = container_stack.back().is_object;
key.clear(); key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key))) if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key, is_object || error_handler == error_handler_t::strict)))
{ {
return false; return false;
} }
// an array's elements are named "0", "1", ... in the wire format, if (is_object && !sax->key(key))
// and those names are not passed on
if (container_stack.back().is_object && !sax->key(key))
{ {
return false; return false;
} }
@@ -489,14 +492,18 @@ class binary_reader
@brief Parses a C-style string from the BSON input. @brief Parses a C-style string from the BSON input.
@param[in,out] result A reference to the string variable where the read @param[in,out] result A reference to the string variable where the read
string is to be stored. string is to be stored.
@param[in] store whether the characters are needed; if not (the names of
an array's elements), they are skipped and @a result may
be left unchanged, and they are not checked against
@ref error_handler
@return `true` if the \\x00-byte indicating the end of the string was @return `true` if the \\x00-byte indicating the end of the string was
encountered before the EOF; false` indicates an unexpected EOF. encountered before the EOF; false` indicates an unexpected EOF.
*/ */
bool get_bson_cstr(string_t& result) bool get_bson_cstr(string_t& result, const bool store = true)
{ {
if (get_bson_cstr_bulk(result, std::integral_constant<bool, bulk_scan> {})) if (get_bson_cstr_bulk(result, store, std::integral_constant<bool, bulk_scan> {}))
{ {
return check_string_utf8(result, "key"); return !store || check_string_utf8(result, "key");
} }
auto out = std::back_inserter(result); auto out = std::back_inserter(result);
@@ -509,7 +516,7 @@ class binary_reader
} }
if (current == 0x00) if (current == 0x00)
{ {
return check_string_utf8(result, "key"); return !store || check_string_utf8(result, "key");
} }
*out++ = static_cast<typename string_t::value_type>(current); *out++ = static_cast<typename string_t::value_type>(current);
} }
@@ -519,10 +526,11 @@ class binary_reader
@brief read a C-style string from contiguous input in one step @brief read a C-style string from contiguous input in one step
@param[in,out] result the string to append to @param[in,out] result the string to append to
@param[in] store whether to append the characters to @a result
@return whether the string was read; if the input has no \\x00-byte, nothing @return whether the string was read; if the input has no \\x00-byte, nothing
is read, and @ref get_bson_cstr reports the end of the input is read, and @ref get_bson_cstr reports the end of the input
*/ */
bool get_bson_cstr_bulk(string_t& result, std::true_type /*bulk*/) bool get_bson_cstr_bulk(string_t& result, const bool store, std::true_type /*bulk*/)
{ {
const std::size_t remaining = ia.bulk_remaining(); const std::size_t remaining = ia.bulk_remaining();
if (remaining == 0) if (remaining == 0)
@@ -530,18 +538,15 @@ class binary_reader
return false; return false;
} }
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data()); const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
// a plain loop rather than std::memchr: most keys are short (array const std::size_t length = find_zero_byte(data, remaining);
// indices are keys, too), and the call would cost more than it saves
std::size_t length = 0;
while (length < remaining && data[length] != 0x00)
{
++length;
}
if (length == remaining) if (length == remaining)
{ {
return false; return false;
} }
result.append(reinterpret_cast<const typename string_t::value_type*>(data), length); if (store)
{
result.append(reinterpret_cast<const typename string_t::value_type*>(data), length);
}
// consume the string and its \x00-byte, as the byte-wise path does // consume the string and its \x00-byte, as the byte-wise path does
ia.bulk_skip(length + 1); ia.bulk_skip(length + 1);
chars_read += length + 1; chars_read += length + 1;
@@ -550,7 +555,7 @@ class binary_reader
} }
/// input that is not contiguous: C-style strings are read byte by byte /// input that is not contiguous: C-style strings are read byte by byte
bool get_bson_cstr_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept bool get_bson_cstr_bulk(string_t& /*result*/, const bool /*store*/, std::false_type /*bulk*/) const noexcept
{ {
return false; return false;
} }
@@ -741,11 +746,13 @@ class binary_reader
bool get_cbor_negative_integer() bool get_cbor_negative_integer()
{ {
NumberType number{}; NumberType number{};
if (JSON_HEDLEY_UNLIKELY(!get_number(number))) return get_number(number) && emit_cbor_negative_integer(number);
{ }
return false;
}
/// pass the CBOR negative integer -1 - @a number to the SAX parser
template<typename NumberType>
bool emit_cbor_negative_integer(const NumberType number)
{
// the value is -1 - number, which fits into number_integer_t // the value is -1 - number, which fits into number_integer_t
// whenever number does; the outer cast undoes the integral promotion // whenever number does; the outer cast undoes the integral promotion
// for number_integer_t types narrower than int // for number_integer_t types narrower than int
@@ -970,7 +977,7 @@ class binary_reader
case 0x95: case 0x95:
case 0x96: case 0x96:
case 0x97: case 0x97:
return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu)); return enter_cbor_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
case 0x98: // array (one-byte uint8_t for n follows) case 0x98: // array (one-byte uint8_t for n follows)
case 0x99: // array (two-byte uint16_t for n follow) case 0x99: // array (two-byte uint16_t for n follow)
@@ -979,7 +986,7 @@ class binary_reader
{ {
std::uint64_t len{}; std::uint64_t len{};
std::size_t size{}; std::size_t size{};
return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_array(size); return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_cbor_array(size);
} }
case 0x9F: // array (indefinite length) case 0x9F: // array (indefinite length)
@@ -1133,16 +1140,10 @@ class binary_reader
return get_half_float(false); return get_half_float(false);
case 0xFA: // Single-Precision Float (four-byte IEEE 754) case 0xFA: // Single-Precision Float (four-byte IEEE 754)
{ return get_cbor_float<float>();
float number{};
return get_number(number) && emit_float(number);
}
case 0xFB: // Double-Precision Float (eight-byte IEEE 754) case 0xFB: // Double-Precision Float (eight-byte IEEE 754)
{ return get_cbor_float<double>();
double number{};
return get_number(number) && emit_float(number);
}
default: // anything else (0xFF is handled inside the other types) default: // anything else (0xFF is handled inside the other types)
{ {
@@ -1151,6 +1152,14 @@ class binary_reader
} }
} }
/// read a single- or double-precision float
template<typename NumberType>
bool get_cbor_float()
{
NumberType number{};
return get_number(number) && emit_float(number);
}
/*! /*!
@brief reports a nested indefinite-length CBOR string or byte array @brief reports a nested indefinite-length CBOR string or byte array
@param[in] type_name name of the rejected string type @param[in] type_name name of the rejected string type
@@ -1676,6 +1685,155 @@ class binary_reader
return true; return true;
} }
/*!
@brief read a big-endian number of type @a NumberType at @a data
*/
template<typename NumberType>
NumberType load_big_endian(const unsigned char* data) const noexcept
{
NumberType result{};
std::memcpy(&result, data, sizeof(NumberType));
if (is_little_endian)
{
byte_swap(result);
}
return result;
}
/*!
@brief read the integers, floats, booleans, and nulls that follow in a
definite-length array, without the per-element bookkeeping of
@ref parse_cbor_internal
Stops at the end of the array, at any other kind of value, and when fewer
than 9 bytes (the longest of these values) are left in the input, leaving
those to @ref parse_cbor_value, which reads them the same way.
Out of line, so that @ref parse_cbor_value stays small enough to be inlined.
The array is the one on top of @ref container_stack; its count of
elements not yet read is updated.
@return whether the SAX parser accepted the values
*/
JSON_HEDLEY_NEVER_INLINE
bool parse_cbor_scalar_run(std::true_type /*bulk*/)
{
// nothing pushes onto the stack while the run reads scalars
std::size_t& remaining = container_stack.back().remaining;
while (remaining != 0 && ia.bulk_remaining() >= 9)
{
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
const unsigned char head = data[0];
std::size_t size = 0;
bool accepted = false;
switch (head)
{
case 0x18:
size = 2;
accepted = emit_unsigned(data[1]);
break;
case 0x19:
size = 3;
accepted = emit_unsigned(load_big_endian<std::uint16_t>(data + 1));
break;
case 0x1A:
size = 5;
accepted = emit_unsigned(load_big_endian<std::uint32_t>(data + 1));
break;
case 0x1B:
size = 9;
accepted = emit_unsigned(load_big_endian<std::uint64_t>(data + 1));
break;
case 0x38:
size = 2;
accepted = emit_cbor_negative_integer(data[1]);
break;
case 0x39:
size = 3;
accepted = emit_cbor_negative_integer(load_big_endian<std::uint16_t>(data + 1));
break;
case 0x3A:
size = 5;
accepted = emit_cbor_negative_integer(load_big_endian<std::uint32_t>(data + 1));
break;
case 0x3B:
size = 9;
accepted = emit_cbor_negative_integer(load_big_endian<std::uint64_t>(data + 1));
break;
case 0xF4:
size = 1;
accepted = sax->boolean(false);
break;
case 0xF5:
size = 1;
accepted = sax->boolean(true);
break;
case 0xF6:
size = 1;
accepted = sax->null();
break;
case 0xFA:
size = 5;
accepted = emit_float(load_big_endian<float>(data + 1));
break;
case 0xFB:
size = 9;
accepted = emit_float(load_big_endian<double>(data + 1));
break;
default:
if (head <= 0x17)
{
size = 1;
accepted = sax->number_unsigned(static_cast<number_unsigned_t>(head));
}
else if (head >= 0x20 && head <= 0x37)
{
size = 1;
accepted = sax->number_integer(static_cast<std::int8_t>(0x20 - 1 - head));
}
else
{
// any other value is read by parse_cbor_value
return true;
}
break;
}
// as if read by get() and get_number(): current is the head byte
ia.bulk_skip(size);
chars_read += size;
current = head;
--remaining;
if (JSON_HEDLEY_UNLIKELY(!accepted))
{
return false;
}
}
return true;
}
/*!
@brief open a definite-length array and read the scalars it begins with
(see @ref parse_cbor_scalar_run)
Only the start of an array is tried, which covers arrays of numbers
(e.g. coordinates) but costs arrays of other values only one call, not one
per element.
@param[in] len the number of elements
@return whether the SAX parser accepted the array and the values read
*/
bool enter_cbor_array(const std::size_t len)
{
return enter_array(len) && (len == 0 || parse_cbor_scalar_run(std::integral_constant<bool, bulk_scan> {}));
}
/// input that is not contiguous: every value is read by parse_cbor_value
bool parse_cbor_scalar_run(std::false_type /*bulk*/) const noexcept
{
return true;
}
/*! /*!
@brief read a CBOR value and everything nested inside it @brief read a CBOR value and everything nested inside it
@@ -1705,6 +1863,7 @@ class binary_reader
// pop_back() below, which destroys the container_stack element // pop_back() below, which destroys the container_stack element
// it would otherwise alias // it would otherwise alias
const container_frame top = container_stack.back(); const container_frame top = container_stack.back();
const bool is_object = top.is_object;
bool at_end = false; bool at_end = false;
if (top.remaining != npos) if (top.remaining != npos)
@@ -1716,7 +1875,7 @@ class binary_reader
{ {
// claim the element about to be read // claim the element about to be read
--container_stack.back().remaining; --container_stack.back().remaining;
if (top.is_object) if (is_object)
{ {
get(); get();
} }
@@ -1729,7 +1888,7 @@ class binary_reader
// Testing for it consumes a byte, which is the first byte // Testing for it consumes a byte, which is the first byte
// of the next element when it is not one. // of the next element when it is not one.
at_end = (get() == 0xFF); at_end = (get() == 0xFF);
fetch = top.is_object; fetch = is_object;
} }
if (at_end) if (at_end)
@@ -1746,7 +1905,7 @@ class binary_reader
continue; continue;
} }
if (top.is_object) if (is_object)
{ {
if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler))) if (JSON_HEDLEY_UNLIKELY(!skip_cbor_key_tags(tag_handler)))
{ {
@@ -1971,7 +2130,7 @@ class binary_reader
case 0x9D: case 0x9D:
case 0x9E: case 0x9E:
case 0x9F: case 0x9F:
return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu)); return enter_msgpack_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
// fixstr // fixstr
case 0xA0: case 0xA0:
@@ -2102,13 +2261,13 @@ class binary_reader
case 0xDC: // array 16 case 0xDC: // array 16
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(len) && enter_array(static_cast<std::size_t>(len)); return get_number(len) && enter_msgpack_array(static_cast<std::size_t>(len));
} }
case 0xDD: // array 32 case 0xDD: // array 32
{ {
std::uint32_t len{}; std::uint32_t len{};
return get_number(len) && enter_array(conditional_static_cast<std::size_t>(len)); return get_number(len) && enter_msgpack_array(conditional_static_cast<std::size_t>(len));
} }
case 0xDE: // map 16 case 0xDE: // map 16
@@ -2450,6 +2609,137 @@ class binary_reader
} }
} }
/*!
@brief read the integers, floats, booleans, and nils that follow in a
MessagePack array, without the per-element bookkeeping of
@ref parse_msgpack_internal
Stops at the end of the array, at any other kind of value, and when fewer
than 9 bytes (the longest of these values) are left in the input, leaving
those to @ref parse_msgpack_value, which reads them the same way.
Out of line, so that @ref parse_msgpack_value stays small enough to be inlined.
The array is the one on top of @ref container_stack; its count of
elements not yet read is updated.
@return whether the SAX parser accepted the values
*/
JSON_HEDLEY_NEVER_INLINE
bool parse_msgpack_scalar_run(std::true_type /*bulk*/)
{
// nothing pushes onto the stack while the run reads scalars
std::size_t& remaining = container_stack.back().remaining;
while (remaining != 0 && ia.bulk_remaining() >= 9)
{
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
const unsigned char head = data[0];
std::size_t size = 0;
bool accepted = false;
switch (head)
{
case 0xC0: // nil
size = 1;
accepted = sax->null();
break;
case 0xC2: // false
size = 1;
accepted = sax->boolean(false);
break;
case 0xC3: // true
size = 1;
accepted = sax->boolean(true);
break;
case 0xCA: // float 32
size = 5;
accepted = emit_float(load_big_endian<float>(data + 1));
break;
case 0xCB: // float 64
size = 9;
accepted = emit_float(load_big_endian<double>(data + 1));
break;
case 0xCC: // uint 8
size = 2;
accepted = emit_unsigned(data[1]);
break;
case 0xCD: // uint 16
size = 3;
accepted = emit_unsigned(load_big_endian<std::uint16_t>(data + 1));
break;
case 0xCE: // uint 32
size = 5;
accepted = emit_unsigned(load_big_endian<std::uint32_t>(data + 1));
break;
case 0xCF: // uint 64
size = 9;
accepted = emit_unsigned(load_big_endian<std::uint64_t>(data + 1));
break;
case 0xD0: // int 8
size = 2;
accepted = emit_signed(static_cast<std::int8_t>(data[1]));
break;
case 0xD1: // int 16
size = 3;
accepted = emit_signed(load_big_endian<std::int16_t>(data + 1));
break;
case 0xD2: // int 32
size = 5;
accepted = emit_signed(load_big_endian<std::int32_t>(data + 1));
break;
case 0xD3: // int 64
size = 9;
accepted = emit_signed(load_big_endian<std::int64_t>(data + 1));
break;
default:
if (head <= 0x7F)
{
// positive fixint
size = 1;
accepted = sax->number_unsigned(static_cast<number_unsigned_t>(head));
}
else if (head >= 0xE0)
{
// negative fixint
size = 1;
accepted = sax->number_integer(static_cast<std::int8_t>(head));
}
else
{
// any other value is read by parse_msgpack_value
return true;
}
break;
}
// as if read by get() and get_number(): current is the last byte
ia.bulk_skip(size);
chars_read += size;
current = data[size - 1];
--remaining;
if (JSON_HEDLEY_UNLIKELY(!accepted))
{
return false;
}
}
return true;
}
/*!
@brief open a MessagePack array and read the scalars it begins with (see
@ref parse_msgpack_scalar_run)
@param[in] len the number of elements
@return whether the SAX parser accepted the array and the values read
*/
bool enter_msgpack_array(const std::size_t len)
{
return enter_array(len) && (len == 0 || parse_msgpack_scalar_run(std::integral_constant<bool, bulk_scan> {}));
}
/// input that is not contiguous: every value is read by parse_msgpack_value
bool parse_msgpack_scalar_run(std::false_type /*bulk*/) const noexcept
{
return true;
}
/*! /*!
@brief read a MessagePack value and everything nested inside it @brief read a MessagePack value and everything nested inside it
@@ -3255,6 +3545,107 @@ class binary_reader
return unexpected_byte("invalid byte", "value"); return unexpected_byte("invalid byte", "value");
} }
/*!
@brief read the elements of an optimized array of numbers (type marker
@a type) that are in the input, in one loop, without the
per-element bookkeeping of @ref parse_ubjson_internal
Values are emitted as @ref get_ubjson_value emits them. Types other than
the fixed-size integers and floats are left to @ref get_ubjson_value, as
are the elements the input ends before.
@param[in,out] remaining the number of elements not yet read
@param[in] type the type marker of the elements
@return whether the SAX parser accepted the values
*/
JSON_HEDLEY_NEVER_INLINE
bool parse_ubjson_typed_run(std::size_t& remaining, const char_int_type type, std::true_type /*bulk*/)
{
const bool bjdata = input_format == input_format_t::bjdata;
switch (type)
{
case 'U':
return parse_ubjson_typed_run<std::uint8_t>(remaining);
case 'i':
return parse_ubjson_typed_run<std::int8_t>(remaining);
case 'I':
return parse_ubjson_typed_run<std::int16_t>(remaining);
case 'l':
return parse_ubjson_typed_run<std::int32_t>(remaining);
case 'L':
return parse_ubjson_typed_run<std::int64_t>(remaining);
case 'u':
return !bjdata || parse_ubjson_typed_run<std::uint16_t>(remaining);
case 'm':
return !bjdata || parse_ubjson_typed_run<std::uint32_t>(remaining);
case 'M':
return !bjdata || parse_ubjson_typed_run<std::uint64_t>(remaining);
case 'd':
return parse_ubjson_typed_run<float>(remaining);
case 'D':
return parse_ubjson_typed_run<double>(remaining);
default:
return true;
}
}
/// input that is not contiguous: every element is read by get_ubjson_value
bool parse_ubjson_typed_run(std::size_t& /*remaining*/, const char_int_type /*type*/, std::false_type /*bulk*/) const noexcept
{
return true;
}
template<typename NumberType>
bool parse_ubjson_typed_run(std::size_t& remaining)
{
constexpr std::size_t size = sizeof(NumberType);
const std::size_t count = (std::min)(remaining, ia.bulk_remaining() / size);
if (count == 0)
{
return true;
}
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
// UBJSON stores numbers big-endian, BJData little-endian
const bool swap = is_little_endian != (input_format == input_format_t::bjdata);
std::size_t read = 0;
bool accepted = true;
while (read < count && accepted)
{
NumberType number{};
std::memcpy(&number, data + (read * size), size);
if (swap)
{
byte_swap(number);
}
++read;
accepted = emit_ubjson_number(number);
}
// as if read by get_number(): current is the last byte read
ia.bulk_skip(read * size);
chars_read += read * size;
current = data[(read * size) - 1];
remaining -= read;
return accepted;
}
template<typename NumberType, enable_if_t<std::is_floating_point<NumberType>::value, int> = 0>
bool emit_ubjson_number(const NumberType number)
{
return emit_float(number);
}
template < typename NumberType, enable_if_t < std::is_integral<NumberType>::value&& std::is_signed<NumberType>::value, int > = 0 >
bool emit_ubjson_number(const NumberType number)
{
return emit_signed(number);
}
template < typename NumberType, enable_if_t < std::is_integral<NumberType>::value&& !std::is_signed<NumberType>::value, int > = 0 >
bool emit_ubjson_number(const NumberType number)
{
return emit_unsigned(number);
}
/*! /*!
@return whether array creation completed @return whether array creation completed
*/ */
@@ -3290,7 +3681,15 @@ class binary_reader
return false; return false;
} }
for (std::size_t i = 0; i < size_and_type.first; ++i) // the numbers that are in the input, in one loop; the rest (if
// the input ends early) one by one below
std::size_t remaining = size_and_type.first;
if (!nd_is_char && JSON_HEDLEY_UNLIKELY(!parse_ubjson_typed_run(remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {})))
{
return false;
}
for (std::size_t i = size_and_type.first - remaining; i < size_and_type.first; ++i)
{ {
if (nd_is_char) if (nd_is_char)
{ {
@@ -3350,7 +3749,8 @@ class binary_reader
container_stack.back().remaining = 0; container_stack.back().remaining = 0;
} }
return true; return size_and_type.second == 0
|| parse_ubjson_typed_run(container_stack.back().remaining, size_and_type.second, std::integral_constant<bool, bulk_scan> {});
} }
return enter_array(detail::unknown_size()); return enter_array(detail::unknown_size());
@@ -3691,13 +4091,13 @@ class binary_reader
return sax->null(); return sax->null();
case 0xFB: case 0xFB:
return sax->number_float(static_cast<number_float_t>(-1.0), ""); return sax->number_float(static_cast<number_float_t>(-1.0), no_source);
case 0xFC: case 0xFC:
return sax->number_float(static_cast<number_float_t>(0.0), ""); return sax->number_float(static_cast<number_float_t>(0.0), no_source);
case 0xFD: case 0xFD:
return sax->number_float(static_cast<number_float_t>(1.0), ""); return sax->number_float(static_cast<number_float_t>(1.0), no_source);
case 0xFF: // empty string case 0xFF: // empty string
{ {
@@ -4020,7 +4420,12 @@ class binary_reader
`char_traits<char_type>::eof()` in that case. `char_traits<char_type>::eof()` in that case.
@return character read from the input @return character read from the input
@note always inlined: GCC otherwise stops inlining the stream adapter's
get_character() into parse_cbor_internal once that function grows
(+0.45 % instructions for CBOR from a std::istringstream with GCC 13)
*/ */
JSON_HEDLEY_ALWAYS_INLINE
char_int_type get() char_int_type get()
{ {
++chars_read; ++chars_read;
@@ -4066,6 +4471,27 @@ class binary_reader
return current; return current;
} }
/// reverse the byte order of a double with a single byteswap(): GCC does
/// not merge the byte-wise swap of the generic version into one
static void byte_swap(double& number) noexcept
{
static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits");
std::uint64_t bits{};
std::memcpy(&bits, &number, sizeof(bits));
bits = byteswap(bits);
std::memcpy(&number, &bits, sizeof(bits));
}
/// reverse the byte order of a float with a single byteswap()
static void byte_swap(float& number) noexcept
{
static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits");
std::uint32_t bits{};
std::memcpy(&bits, &number, sizeof(bits));
bits = byteswap(bits);
std::memcpy(&number, &bits, sizeof(bits));
}
template<class NumberType> template<class NumberType>
static void byte_swap(NumberType& number) static void byte_swap(NumberType& number)
{ {
@@ -4201,7 +4627,7 @@ class binary_reader
{ {
return out_of_range_error(exception_id::number_overflow, "number overflow", "value"); return out_of_range_error(exception_id::number_overflow, "number overflow", "value");
} }
return sax->number_float(result, ""); return sax->number_float(result, no_source);
} }
/*! /*!
@@ -4261,7 +4687,7 @@ class binary_reader
}(); }();
return sax->number_float((half & 0x8000u) != 0 return sax->number_float((half & 0x8000u) != 0
? static_cast<number_float_t>(-val) ? static_cast<number_float_t>(-val)
: static_cast<number_float_t>(val), ""); : static_cast<number_float_t>(val), no_source);
} }
/*! /*!
@@ -4293,6 +4719,19 @@ class binary_reader
return get_bytes(len, "string", result); return get_bytes(len, "string", result);
} }
/// whether @a s is valid UTF-8: ASCII eight bytes at a time, the rest
/// sequence by sequence (as the binary writers check strings)
static bool is_valid_utf8_string(const string_t& s, std::true_type /*is_byte*/) noexcept
{
return valid_utf8_string(reinterpret_cast<const unsigned char*>(s.data()), s.size());
}
/// whether @a s, of a character type wider than a byte, is valid UTF-8
static bool is_valid_utf8_string(const string_t& s, std::false_type /*is_byte*/) noexcept
{
return is_valid_utf8(s);
}
/*! /*!
@brief validate a decoded text string (value or object key) against @ref error_handler @brief validate a decoded text string (value or object key) against @ref error_handler
@@ -4312,7 +4751,16 @@ class binary_reader
*/ */
bool check_string_utf8(string_t& result, const char* context) bool check_string_utf8(string_t& result, const char* context)
{ {
if (error_handler == error_handler_t::keep || is_valid_utf8(result)) return error_handler == error_handler_t::keep || check_string_utf8_handler(result, context);
}
/// @ref check_string_utf8 for an error handler other than @ref
/// error_handler_t::keep; out of line, so that reading strings without a
/// check does not pay for the code of the check
JSON_HEDLEY_NEVER_INLINE
bool check_string_utf8_handler(string_t& result, const char* context)
{
if (is_valid_utf8_string(result, std::integral_constant<bool, sizeof(typename string_t::value_type) == 1>()))
{ {
return true; return true;
} }
@@ -4586,6 +5034,10 @@ class binary_reader
/// the SAX parser /// the SAX parser
json_sax_t* sax = nullptr; json_sax_t* sax = nullptr;
/// the (empty) source text of every number_float event: binary formats
/// have none, and a string_t built for each float would cost more
const string_t no_source{};
/// the containers that have been opened and not closed yet; see @ref container_frame /// the containers that have been opened and not closed yet; see @ref container_frame
std::vector<container_frame> container_stack{}; std::vector<container_frame> container_stack{};
@@ -545,12 +545,9 @@ class input_stream_adapter
std::char_traits<char>::int_type get_character_slow() std::char_traits<char>::int_type get_character_slow()
{ {
#if !JSON_DISABLE_STREAM_BULK_SCAN #if !JSON_DISABLE_STREAM_BULK_SCAN
// sbumpc() refills the get area if it is exhausted // the get area is exhausted (get_character() handles the other case),
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); // so sbumpc() refills it
if (JSON_HEDLEY_UNLIKELY(refill)) save_token_segment();
{
save_token_segment();
}
#endif #endif
auto res = sb->sbumpc(); auto res = sb->sbumpc();
@@ -563,17 +560,13 @@ class input_stream_adapter
is->clear(is->rdstate() | std::ios::eofbit); is->clear(is->rdstate() | std::ios::eofbit);
} }
#if !JSON_DISABLE_STREAM_BULK_SCAN #if !JSON_DISABLE_STREAM_BULK_SCAN
else if (JSON_HEDLEY_UNLIKELY(refill)) else
{ {
// the consumed character is at gptr() - 1, unless the streambuf // the consumed character is at gptr() - 1, unless the streambuf
// has no get area and returned it from uflow() // has no get area and returned it from uflow()
const char* const next = streambuf_access::get_ptr(sb); const char* const next = streambuf_access::get_ptr(sb);
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
} }
else
{
current_in_get_area = true;
}
#endif #endif
return res; return res;
} }
+4 -5
View File
@@ -999,7 +999,7 @@ class json_sax_dom_parser : public json_sax_dom_error_state
ordered_data.members[f.ordered.dest_member].second = std::move(value); ordered_data.members[f.ordered.dest_member].second = std::move(value);
} }
static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} static void place_member(const frame& /*f*/, BasicJsonType&& /*value*/, std::false_type /*indexed*/) noexcept {} // LCOV_EXCL_LINE
/// store the members of a new object directly in it /// store the members of a new object directly in it
bool start_stored_object(std::size_t len) bool start_stored_object(std::size_t len)
@@ -1737,7 +1737,7 @@ class json_sax_dom_callback_parser : public json_sax_dom_error_state
key_stack.push_back(val); key_stack.push_back(val);
// add discarded value at the given key and store the reference for later // add discarded value at the given key and store the reference for later
if (keep && ref_stack.back()) if (keep)
{ {
auto& obj = *ref_stack.back()->m_data.m_value.object; auto& obj = *ref_stack.back()->m_data.m_value.object;
// a single lookup: insert the placeholder, or find the existing member // a single lookup: insert the placeholder, or find the existing member
@@ -1794,7 +1794,7 @@ class json_sax_dom_callback_parser : public json_sax_dom_error_state
const string_t object_key = pop_container(); const string_t object_key = pop_container();
if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_structured()) if (!ref_stack.empty() && ref_stack.back())
{ {
// remove discarded value // remove discarded value
remove_discarded_value(*ref_stack.back(), object_key); remove_discarded_value(*ref_stack.back(), object_key);
@@ -1960,8 +1960,7 @@ class json_sax_dom_callback_parser : public json_sax_dom_error_state
*/ */
string_t current_key() const string_t current_key() const
{ {
if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_object() if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_object())
&& !key_stack.empty())
{ {
return key_stack.back(); return key_stack.back();
} }
+113 -65
View File
@@ -209,8 +209,13 @@ constexpr bool input_adapter_bulk_window(std::false_type /*detected*/)
@brief lexical analysis @brief lexical analysis
This class organizes the lexical analysis during JSON deserialization. This class organizes the lexical analysis during JSON deserialization.
DiscardValues: the caller only needs the token types and never looks at the values of number and string tokens (only
accept(), which parses through json_sax_acceptor). A template parameter rather than a member, so that the lexer of
parse() is compiled without the paths of accept(): with both string scans in one scan(), parse() was up to 2 % slower
on Apple M1.
*/ */
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false>
class lexer : public lexer_base<BasicJsonType> class lexer : public lexer_base<BasicJsonType>
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
@@ -268,10 +273,9 @@ class lexer : public lexer_base<BasicJsonType>
public: public:
using token_type = typename lexer_base<BasicJsonType>::token_type; using token_type = typename lexer_base<BasicJsonType>::token_type;
explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false, bool discard_number_values_ = false) noexcept explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false) noexcept
: ia(std::move(adapter)) : ia(std::move(adapter))
, ignore_comments(ignore_comments_) , ignore_comments(ignore_comments_)
, discard_number_values(discard_number_values_)
{ {
// the token string of an error before the first token is everything // the token string of an error before the first token is everything
// read so far // read so far
@@ -296,14 +300,16 @@ class lexer : public lexer_base<BasicJsonType>
/// calls would (including the position and token bookkeeping of a /// calls would (including the position and token bookkeeping of a
/// streaming adapter, see bulk_consume) and leaves @a current holding the last of /// streaming adapter, see bulk_consume) and leaves @a current holding the last of
/// the 4 digits, just as the last such get() would; the codepoint is /// the 4 digits, just as the last such get() would; the codepoint is
/// written to @a out. Makes no state change and returns false for a /// written to @a out. Makes no state change and returns false for fewer
/// pending unget, fewer than 4 remaining bytes, or any of the 4 bytes not /// than 4 remaining bytes or any of the 4 bytes not being a hex digit, so
/// being a hex digit, so the caller falls back unchanged to the /// the caller falls back unchanged to the per-character loop, which then
/// per-character loop, which then reports the same diagnostic (stopping /// reports the same diagnostic (stopping at the first invalid digit) as
/// at the first invalid digit) as before this optimization. /// before this optimization.
bool get_codepoint_bulk(std::true_type /*bulk*/, int& out) bool get_codepoint_bulk(std::true_type /*bulk*/, int& out)
{ {
if (next_unget || ia.bulk_remaining() < 4) // no unget is pending inside a string (see get_after_string_run())
JSON_ASSERT(!next_unget);
if (ia.bulk_remaining() < 4)
{ {
return false; return false;
} }
@@ -388,6 +394,8 @@ class lexer : public lexer_base<BasicJsonType>
checks if it is inside the range. If a violation was detected, set up an checks if it is inside the range. If a violation was detected, set up an
error message and return false. Otherwise, return true. error message and return false. Otherwise, return true.
@param[in] keep whether the bytes are added to token_buffer (false for
accept(), see scan())
@param[in] ranges list of integers; interpreted as list of pairs of @param[in] ranges list of integers; interpreted as list of pairs of
inclusive lower and upper bound, respectively inclusive lower and upper bound, respectively
@@ -396,17 +404,18 @@ class lexer : public lexer_base<BasicJsonType>
@return true if and only if no range violation was detected @return true if and only if no range violation was detected
*/ */
bool next_byte_in_range(std::initializer_list<char_int_type> ranges) template<bool Keep>
bool next_byte_in_range(std::integral_constant<bool, Keep> keep, std::initializer_list<char_int_type> ranges)
{ {
JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6); JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6);
add(current); add_string_char(keep, current);
for (auto range = ranges.begin(); range != ranges.end(); ++range) for (auto range = ranges.begin(); range != ranges.end(); ++range)
{ {
get(); get();
if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions) if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions)
{ {
add(current); add_string_char(keep, current);
} }
else else
{ {
@@ -419,20 +428,21 @@ class lexer : public lexer_base<BasicJsonType>
} }
/// contiguous input: bulk-append the run of ordinary characters and complete /// contiguous input: bulk-append the run of ordinary characters and complete
/// well-formed UTF-8 sequences starting at the current read position, then /// well-formed UTF-8 sequences starting at the current read position (only
/// skip it if @a keep is false), then
/// read the first byte that needs individual handling (the closing quote, an /// read the first byte that needs individual handling (the closing quote, an
/// escape, a control character, or an ill-formed UTF-8 byte) like get(). /// escape, a control character, or an ill-formed UTF-8 byte) like get().
/// That byte is taken from the buffer that was just scanned. Through get(), /// That byte is taken from the buffer that was just scanned. Through get(),
/// Apple Clang merged this read with the other get() calls of the scan /// Apple Clang merged this read with the other get() calls of the scan
/// function, which load the read position again right after it was stored /// function, which load the read position again right after it was stored
/// (3% of json::accept() on jeopardy.json on Apple M1). /// (3% of json::accept() on jeopardy.json on Apple M1).
char_int_type get_after_string_run(std::true_type /*bulk*/) template<bool Keep>
char_int_type get_after_string_run(std::true_type /*bulk*/, std::integral_constant<bool, Keep> keep)
{ {
// a pending unget must be consumed through the normal path first // scan_string() is entered from scan_after_whitespace(), whose current
if (next_unget) // was read by skip_whitespace() (its first get() clears a pending
{ // unget), and nothing in scan_string() calls unget()
return get(); JSON_ASSERT(!next_unget);
}
const std::size_t remaining = ia.bulk_remaining(); const std::size_t remaining = ia.bulk_remaining();
if (remaining == 0) if (remaining == 0)
{ {
@@ -444,7 +454,7 @@ class lexer : public lexer_base<BasicJsonType>
const std::size_t pos = lexer_string_bulk_run(data, remaining); const std::size_t pos = lexer_string_bulk_run(data, remaining);
if (pos != 0) if (pos != 0)
{ {
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos); append_string_run(keep, data, pos);
bulk_consume(reinterpret_cast<const char*>(data), pos); bulk_consume(reinterpret_cast<const char*>(data), pos);
} }
if (pos == remaining) if (pos == remaining)
@@ -488,7 +498,8 @@ class lexer : public lexer_base<BasicJsonType>
} }
/// streaming input: no bulk fast path /// streaming input: no bulk fast path
char_int_type get_after_string_run(std::false_type /*bulk*/) template<bool Keep>
char_int_type get_after_string_run(std::false_type /*bulk*/, std::integral_constant<bool, Keep> /*keep*/)
{ {
return get(); return get();
} }
@@ -502,13 +513,18 @@ class lexer : public lexer_base<BasicJsonType>
may contain \0 bytes), and token_buffer.size() is the number of bytes in the may contain \0 bytes), and token_buffer.size() is the number of bytes in the
string. string.
@param[in] keep whether the string is copied into token_buffer; if false
(accept(), see scan()), it is only checked, with the same
errors, and token_buffer stays empty
@return token_type::value_string if string could be successfully scanned, @return token_type::value_string if string could be successfully scanned,
token_type::parse_error otherwise token_type::parse_error otherwise
@note In case of errors, variable error_message contains a textual @note In case of errors, variable error_message contains a textual
description. description.
*/ */
token_type scan_string() template<bool Keep>
token_type scan_string(std::integral_constant<bool, Keep> keep)
{ {
// reset token_buffer (ignore opening quote) // reset token_buffer (ignore opening quote)
reset(); reset();
@@ -520,7 +536,7 @@ class lexer : public lexer_base<BasicJsonType>
{ {
// bulk-consume ordinary characters from contiguous input, then // bulk-consume ordinary characters from contiguous input, then
// get the next character, the one that ends the run // get the next character, the one that ends the run
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>())) switch (get_after_string_run(std::integral_constant<bool, bulk_scan>(), keep))
{ {
// end of file while parsing the string // end of file while parsing the string
case char_traits<char_type>::eof(): case char_traits<char_type>::eof():
@@ -542,35 +558,35 @@ class lexer : public lexer_base<BasicJsonType>
{ {
// quotation mark // quotation mark
case '\"': case '\"':
add('\"'); add_string_char(keep, '\"');
break; break;
// reverse solidus // reverse solidus
case '\\': case '\\':
add('\\'); add_string_char(keep, '\\');
break; break;
// solidus // solidus
case '/': case '/':
add('/'); add_string_char(keep, '/');
break; break;
// backspace // backspace
case 'b': case 'b':
add('\b'); add_string_char(keep, '\b');
break; break;
// form feed // form feed
case 'f': case 'f':
add('\f'); add_string_char(keep, '\f');
break; break;
// line feed // line feed
case 'n': case 'n':
add('\n'); add_string_char(keep, '\n');
break; break;
// carriage return // carriage return
case 'r': case 'r':
add('\r'); add_string_char(keep, '\r');
break; break;
// tab // tab
case 't': case 't':
add('\t'); add_string_char(keep, '\t');
break; break;
// unicode escapes // unicode escapes
@@ -638,10 +654,7 @@ class lexer : public lexer_base<BasicJsonType>
JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF); JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF);
// translate codepoint into bytes // translate codepoint into bytes
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte) add_codepoint(keep, codepoint);
{
add(static_cast<char_int_type>(byte));
});
break; break;
} }
@@ -824,7 +837,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0x7E: case 0x7E:
case 0x7F: case 0x7F:
{ {
add(current); add_string_char(keep, current);
break; break;
} }
@@ -860,7 +873,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xDE: case 0xDE:
case 0xDF: case 0xDF:
{ {
if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF}))) if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range(keep, {0x80, 0xBF})))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -870,7 +883,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+0800..U+0FFF: bytes E0 A0..BF 80..BF // U+0800..U+0FFF: bytes E0 A0..BF 80..BF
case 0xE0: case 0xE0:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0xA0, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -894,7 +907,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xEE: case 0xEE:
case 0xEF: case 0xEF:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -904,7 +917,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+D000..U+D7FF: bytes ED 80..9F 80..BF // U+D000..U+D7FF: bytes ED 80..9F 80..BF
case 0xED: case 0xED:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x9F, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -914,7 +927,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+10000..U+3FFFF F0 90..BF 80..BF 80..BF // U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
case 0xF0: case 0xF0:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -926,7 +939,7 @@ class lexer : public lexer_base<BasicJsonType>
case 0xF2: case 0xF2:
case 0xF3: case 0xF3:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -936,7 +949,7 @@ class lexer : public lexer_base<BasicJsonType>
// U+100000..U+10FFFF F4 80..8F 80..BF 80..BF // U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
case 0xF4: case 0xF4:
{ {
if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF})))) if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range(keep, {0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF}))))
{ {
return token_type::parse_error; return token_type::parse_error;
} }
@@ -1398,7 +1411,7 @@ scan_number_done:
return token_type::value_unsigned; return token_type::value_unsigned;
} }
} }
else if (number_type == token_type::value_integer) else // value_integer (the callers handle value_float themselves)
{ {
if (parse_integer_signed(first, last, value_integer)) if (parse_integer_signed(first, last, value_integer))
{ {
@@ -1427,7 +1440,7 @@ scan_number_done:
token_type convert_number(token_type number_type, std::size_t mantissa_end) token_type convert_number(token_type number_type, std::size_t mantissa_end)
{ {
// accept() only needs to know whether the input is valid, so it sets // accept() only needs to know whether the input is valid, so it sets
// discard_number_values (see json.hpp), and an integer token whose // discard_values (see json.hpp), and an integer token whose
// digit count shows that it fits is reported without calling // digit count shows that it fits is reported without calling
// convert_integer(). A number with up to 18 digits always fits into // convert_integer(). A number with up to 18 digits always fits into
// both std::uint64_t and std::int64_t (18 nines is about 1e18, below // both std::uint64_t and std::int64_t (18 nines is about 1e18, below
@@ -1442,7 +1455,7 @@ scan_number_done:
// json_sax_acceptor, whose number callbacks discard their argument and // json_sax_acceptor, whose number callbacks discard their argument and
// return true, and the parser rejects neither integers nor finite // return true, and the parser rejects neither integers nor finite
// floats. value_unsigned/value_integer are left unset here, so a caller // floats. value_unsigned/value_integer are left unset here, so a caller
// that reads the converted value must not set discard_number_values. // that reads the converted value must not set discard_values.
// //
// On contiguous input, scan_number_bulk_contiguous() converts integer // On contiguous input, scan_number_bulk_contiguous() converts integer
// tokens itself and does not pass them to this function, unless // tokens itself and does not pass them to this function, unless
@@ -1450,7 +1463,7 @@ scan_number_done:
// reached for input without bulk access (e.g. streams), with // reached for input without bulk access (e.g. streams), with
// JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous() // JSON_DIAGNOSTIC_POSITIONS, or when scan_number_bulk_contiguous()
// falls back to scan_number(). // falls back to scan_number().
if (discard_number_values) if (discard_values())
{ {
constexpr std::size_t safe_digit_count = 18; constexpr std::size_t safe_digit_count = 18;
if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count) if (number_type == token_type::value_unsigned && token_buffer.size() <= safe_digit_count)
@@ -1504,11 +1517,9 @@ scan_number_done:
*/ */
token_type scan_number_bulk_contiguous() token_type scan_number_bulk_contiguous()
{ {
// a pending unget offsets the buffer position from current; fall back // called right after scan_after_whitespace() read current (see
if (next_unget) // get_after_string_run()), so no unget is pending
{ JSON_ASSERT(!next_unget);
return token_type::uninitialized;
}
const std::size_t rem = ia.bulk_remaining(); const std::size_t rem = ia.bulk_remaining();
if (rem == 0) if (rem == 0)
{ {
@@ -1529,11 +1540,7 @@ scan_number_done:
if (data[0] == '-') if (data[0] == '-')
{ {
number_type = token_type::value_integer; number_type = token_type::value_integer;
i = 1; i = 1; // avail >= 2: rem == 0 returned above
if (i >= avail)
{
return token_type::uninitialized;
}
} }
const std::size_t integer_begin = i; const std::size_t integer_begin = i;
if (data[i] == '0') if (data[i] == '0')
@@ -1701,7 +1708,7 @@ scan_number_done:
// w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a // w < 10^19, so a value below 10^max_exponent10 cannot overflow, and a
// value that underflows is a finite number as well. Only a token that // value that underflows is a finite number as well. Only a token that
// may overflow has to be converted to find out. // may overflow has to be converted to find out.
if (JSON_HEDLEY_UNLIKELY(discard_number_values && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10)) if (JSON_HEDLEY_UNLIKELY(discard_values() && s.exponent + 19 <= std::numeric_limits<number_float_t>::max_exponent10))
{ {
value_float = 0; value_float = 0;
return true; return true;
@@ -1775,7 +1782,10 @@ scan_number_done:
/// the error at the right character /// the error at the right character
JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/) JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/)
{ {
if (next_unget || ia.bulk_remaining() < length // called right after scan_after_whitespace() read current (see
// get_after_string_run()), so no unget is pending
JSON_ASSERT(!next_unget);
if (ia.bulk_remaining() < length
|| std::memcmp(ia.bulk_data(), rest, length) != 0) || std::memcmp(ia.bulk_data(), rest, length) != 0)
{ {
return false; return false;
@@ -2033,6 +2043,34 @@ scan_number_done:
token_buffer.push_back(static_cast<typename string_t::value_type>(c)); token_buffer.push_back(static_cast<typename string_t::value_type>(c));
} }
/// add a character of a string token; accept() checks the string but does
/// not keep its value (@a keep is false)
void add_string_char(std::true_type /*keep*/, char_int_type c)
{
token_buffer.push_back(static_cast<typename string_t::value_type>(c));
}
static void add_string_char(std::false_type /*keep*/, char_int_type /*c*/) noexcept {}
/// add the UTF-8 bytes of a code point from a \\u escape to a string token
void add_codepoint(std::true_type /*keep*/, int codepoint)
{
encode_utf8(static_cast<std::uint32_t>(codepoint), [this](std::uint32_t byte)
{
add(static_cast<char_int_type>(byte));
});
}
static void add_codepoint(std::false_type /*keep*/, int /*codepoint*/) noexcept {}
/// append a run of ordinary string characters (see get_after_string_run())
void append_string_run(std::true_type /*keep*/, const unsigned char* data, std::size_t n)
{
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), n);
}
static void append_string_run(std::false_type /*keep*/, const unsigned char* /*data*/, std::size_t /*n*/) noexcept {}
public: public:
///////////////////// /////////////////////
// value getters // value getters
@@ -2427,7 +2465,9 @@ scan_number_done:
// string // string
case '\"': case '\"':
return scan_string(); // accept() (DiscardValues) checks strings without copying them
// into token_buffer
return scan_string(keep_strings {});
// number // number
case '-': case '-':
@@ -2512,11 +2552,19 @@ scan_number_done:
std::size_t decimal_point_position = std::string::npos; std::size_t decimal_point_position = std::string::npos;
/// whether the caller only needs the token types and never looks at the /// whether the caller only needs the token types and never looks at the
/// converted numeric values; set only by accept(), which parses through /// values of number and string tokens; DiscardValues, set only by accept(), which parses
/// json_sax_acceptor. When set, convert_number() skips converting integer /// through json_sax_acceptor. When set, convert_number() skips converting
/// tokens whose digit count guarantees that they fit into 64 bits (see /// integer tokens whose digit count guarantees that they fit into 64 bits
/// there) /// (see there), floats are converted only when they may overflow (see
const bool discard_number_values = false; /// convert_accumulated_float()), and strings are checked without being
/// copied into token_buffer (see scan())
static bool discard_values() noexcept
{
return DiscardValues;
}
/// the tag of scan_string(): whether string tokens are kept
using keep_strings = typename std::conditional<DiscardValues, std::false_type, std::true_type>::type;
}; };
} // namespace detail } // namespace detail
+6 -10
View File
@@ -72,14 +72,14 @@ This class implements a parser for JSON text. Nested arrays and objects are trac
stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read stack instead of recursion, so deeply nested input does not exhaust the call stack, and what is read
is reported as SAX events. is reported as SAX events.
*/ */
template<typename BasicJsonType, typename InputAdapterType> template<typename BasicJsonType, typename InputAdapterType, bool DiscardValues = false>
class parser class parser
{ {
using number_integer_t = typename BasicJsonType::number_integer_t; using number_integer_t = typename BasicJsonType::number_integer_t;
using number_unsigned_t = typename BasicJsonType::number_unsigned_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
using number_float_t = typename BasicJsonType::number_float_t; using number_float_t = typename BasicJsonType::number_float_t;
using string_t = typename BasicJsonType::string_t; using string_t = typename BasicJsonType::string_t;
using lexer_t = lexer<BasicJsonType, InputAdapterType>; using lexer_t = lexer<BasicJsonType, InputAdapterType, DiscardValues>;
using token_type = typename lexer_t::token_type; using token_type = typename lexer_t::token_type;
public: public:
@@ -88,10 +88,9 @@ class parser
parser_callback_t<BasicJsonType> cb = nullptr, parser_callback_t<BasicJsonType> cb = nullptr,
const bool allow_exceptions_ = true, const bool allow_exceptions_ = true,
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas_ = false, const bool ignore_trailing_commas_ = false)
const bool discard_number_values_ = false)
: callback(std::move(cb)) : callback(std::move(cb))
, m_lexer(std::move(adapter), ignore_comments, discard_number_values_) , m_lexer(std::move(adapter), ignore_comments)
, allow_exceptions(allow_exceptions_) , allow_exceptions(allow_exceptions_)
, ignore_trailing_commas(ignore_trailing_commas_) , ignore_trailing_commas(ignore_trailing_commas_)
{ {
@@ -576,10 +575,7 @@ class parser
{ {
std::string error_msg = "syntax error "; std::string error_msg = "syntax error ";
if (!context.empty()) error_msg += concat("while parsing ", context, ' ');
{
error_msg += concat("while parsing ", context, ' ');
}
error_msg += "- "; error_msg += "- ";
@@ -599,7 +595,7 @@ class parser
} }
return error_msg; return error_msg;
} } // LCOV_EXCL_LINE (exception cleanup only)
private: private:
/// callback function /// callback function
@@ -322,6 +322,78 @@ JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(cons
block_out = block; block_out = block;
return false; return false;
} }
/*!
@brief skip 16-byte blocks of well-formed UTF-8 at p (a character boundary)
Like vector_utf8_run, but quotes, backslashes, and control characters are
ordinary characters. Returns the start of the first block in which the check
found an error (which may belong to a sequence begun in the block before) or
where fewer than 16 bytes are left; a sequence may be cut at that point, so the
caller resumes at its start, one sequence at a time.
*/
JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline const unsigned char* vector_utf8_valid_blocks(const unsigned char* block, const unsigned char* e) noexcept
{
using lookup = utf8_lookup4<>;
#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);
if (vmaxvq_u8(vorrq_u8(in, prev)) < 0x80)
{
// ASCII here, and no sequence from the block before to finish
prev = in;
block += 16;
continue;
}
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);
if (vmaxvq_u8(err) != 0)
{
break;
}
prev = in;
block += 16;
}
#else
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)
if (_mm_movemask_epi8(_mm_or_si128(in, prev)) == 0)
{
prev = in;
block += 16;
continue;
}
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);
if (_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero)) != 0xFFFF)
{
break;
}
prev = in;
block += 16;
}
#endif
return block;
}
#endif #endif
} // namespace detail } // namespace detail
@@ -90,6 +90,34 @@ JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* d
return n; return n;
} }
// return the index of the first 0x00 byte in [data, data+n), or n if there is
// none; scans 8 bytes at a time (BSON's C-style strings)
JSON_HEDLEY_ALWAYS_INLINE std::size_t find_zero_byte(const unsigned char* data, std::size_t n) noexcept
{
constexpr std::uint64_t ones = 0x0101010101010101ull;
constexpr std::uint64_t high = 0x8080808080808080ull;
std::size_t i = 0;
for (; i + 8 <= n; i += 8)
{
const std::uint64_t v = read_eight_bytes(data + i);
const std::uint64_t zero = (v - ones) & ~v & high;
if (zero != 0)
{
// the lowest flagged byte is the first zero byte (see
// find_string_special())
return i + (static_cast<std::size_t>(count_trailing_zeros(zero)) / 8);
}
}
for (; i < n; ++i)
{
if (data[i] == 0x00)
{
return i;
}
}
return n;
}
// classify a byte as one the serializer must NOT copy verbatim when // classify a byte as one the serializer must NOT copy verbatim when
// ensure_ascii is requested: the closing quote, an escape, a control character // ensure_ascii is requested: the closing quote, an escape, a control character
// (< 0x20), DEL (0x7F), or any non-ASCII byte (>= 0x80). Everything else - // (< 0x20), DEL (0x7F), or any non-ASCII byte (>= 0x80). Everything else -
@@ -236,6 +264,64 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n
return pos; return pos;
} }
/*!
@brief whether [data, data+n) is valid UTF-8, for checking whole strings
The same answer as valid_utf8_prefix(data, n) == n. Short ASCII strings, the
most common keys and values, are decided in one test, and long strings are
checked 16 bytes at a time where the vector check is available.
*/
inline bool valid_utf8_string(const unsigned char* data, std::size_t n) noexcept
{
constexpr std::uint64_t high = 0x8080808080808080ull;
if (n <= 16)
{
// two overlapping words cover 8 to 16 bytes, one partial word fewer
std::uint64_t first = 0;
std::uint64_t last = 0;
if (n >= 8)
{
std::memcpy(&first, data, sizeof(first));
std::memcpy(&last, data + n - 8, sizeof(last));
}
else if (n != 0)
{
std::memcpy(&first, data, n);
}
if (((first | last) & high) == 0)
{
return true;
}
}
#if JSON_SCAN_VECTOR_UTF8
else if (n >= 32
#if JSON_SCAN_SSSE3_DISPATCH
&& cpu_has_ssse3()
#endif
)
{
const unsigned char* block = vector_utf8_valid_blocks(data, data + n);
// the rest from the start of a sequence that may be cut at block
for (int i = 1; i <= 3 && block - i >= data; ++i)
{
const unsigned char c = block[-i];
if (c < 0x80)
{
break;
}
if (c >= 0xC0)
{
block -= i;
break;
}
}
const auto checked = static_cast<std::size_t>(block - data);
return checked + valid_utf8_prefix(block, n - checked) == n;
}
#endif
return valid_utf8_prefix(data, n) == n;
}
#if JSON_SCAN_VECTOR_UTF8 #if JSON_SCAN_VECTOR_UTF8
/// the end of scan_string_vector from block, where the vector loop stopped /// 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 /// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a
@@ -108,6 +108,9 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
// m_it is already value-initialized, so this is not needed for
// correctness; it is kept because GCC generates faster loops with it
// (iterating a parsed document is 0.5-2.7 % slower without it, #6005)
if (m_object->is_object()) if (m_object->is_object())
{ {
m_it.object_iterator = typename object_t::iterator(); m_it.object_iterator = typename object_t::iterator();
+4 -4
View File
@@ -304,7 +304,7 @@ class json_pointer
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end()); result.reference_tokens.insert(result.reference_tokens.end(), rhs.reference_tokens.begin(), rhs.reference_tokens.end());
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer /// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
@@ -315,7 +315,7 @@ class json_pointer
result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end()); result.reference_tokens.insert(result.reference_tokens.end(), lhs.reference_tokens.begin(), lhs.reference_tokens.end());
result.reference_tokens.push_back(std::move(token)); result.reference_tokens.push_back(std::move(token));
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer /// @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/ /// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
@@ -2180,7 +2180,7 @@ class json_pointer
{ {
// create an entry in the array // create an entry in the array
current = &current->operator[](array_index<BasicJsonType>(token)); current = &current->operator[](array_index<BasicJsonType>(token));
break; break; // LCOV_EXCL_LINE (needs an array index with 10 or more digits: more than 1e9 elements)
} }
/* /*
@@ -2217,7 +2217,7 @@ class json_pointer
json_pointer<string_t> result; json_pointer<string_t> result;
result.reference_tokens = reference_tokens; result.reference_tokens = reference_tokens;
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
json_pointer<string_t> convert()&& json_pointer<string_t> convert()&&
{ {
+1 -1
View File
@@ -347,7 +347,7 @@
return ej_pair.second == j; \ return ej_pair.second == j; \
}); \ }); \
if (it != std::end(m)) e = it->first; \ if (it != std::end(m)) e = it->first; \
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat(nlohmann::detail::exception_prefix(j), "enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
} }
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They // Ugly macros to avoid uglier copy-paste when specializing basic_json. They
+340 -124
View File
@@ -833,7 +833,7 @@ class binary_writer
while (n != 0); while (n != 0);
std::reverse(result.begin(), result.end()); std::reverse(result.begin(), result.end());
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/*! /*!
@brief the decimal digits of the integer @a n, with a leading '-' if it @brief the decimal digits of the integer @a n, with a leading '-' if it
@@ -860,7 +860,7 @@ class binary_writer
} }
std::reverse(result.begin(), result.end()); std::reverse(result.begin(), result.end());
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/*! /*!
@brief throws because the integer @a j is too large for @a format @brief throws because the integer @a j is too large for @a format
@@ -1792,7 +1792,7 @@ class binary_writer
std::memcpy(out, &n, sizeof(NumberType)); std::memcpy(out, &n, sizeof(NumberType));
if (!is_little_endian) if (!is_little_endian)
{ {
std::reverse(out, out + sizeof(NumberType)); std::reverse(out, out + sizeof(NumberType)); // LCOV_EXCL_LINE (big-endian hosts only)
} }
} }
@@ -1921,7 +1921,7 @@ class binary_writer
const string_t& value = *j.m_data.m_value.string; const string_t& value = *j.m_data.m_value.string;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size() + 1ul))) if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(value.size() + 1ul)))
{ {
return false; return false; // LCOV_EXCL_LINE (a string of 2 GiB or more)
} }
out[0] = to_char_type(0x02); out[0] = to_char_type(0x02);
store_little_endian<std::int32_t>(p, static_cast<std::int32_t>(value.size() + 1ul)); store_little_endian<std::int32_t>(p, static_cast<std::int32_t>(value.size() + 1ul));
@@ -2088,7 +2088,8 @@ class binary_writer
/// arrays: the first index with one more digit; only maintained by /// arrays: the first index with one more digit; only maintained by
/// @ref calc_bson_sizes /// @ref calc_bson_sizes
std::uint64_t index_boundary = 10; std::uint64_t index_boundary = 10;
/// @ref calc_bson_sizes only: where its size goes in the table /// @ref calc_bson_sizes: where its size goes in the table; @ref
/// write_bson_document_single_pass: the position of its size field
std::size_t size_slot; std::size_t size_slot;
/// @ref calc_bson_sizes only: the size of its entries seen so far /// @ref calc_bson_sizes only: the size of its entries seen so far
std::size_t entries_size = 0; std::size_t entries_size = 0;
@@ -2339,6 +2340,199 @@ class binary_writer
} }
} }
template<typename SinkType>
using sink_written_t = decltype(std::declval<const SinkType&>().written());
/// whether every integer fits the 64 bits BSON has, so that writing an
/// array of numbers needs no range check
static constexpr bool bson_integers_fit_64_bits =
std::numeric_limits<typename BasicJsonType::number_integer_t>::is_specialized
&& std::numeric_limits<typename BasicJsonType::number_integer_t>::digits <= std::numeric_limits<std::int64_t>::digits
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::is_specialized
&& std::numeric_limits<typename BasicJsonType::number_unsigned_t>::digits <= std::numeric_limits<std::uint64_t>::digits;
/*!
@brief whether BSON documents can be written in a single pass: the sink
can overwrite what it has written (output_vector_sink), and
@ref bson_integers_fit_64_bits
*/
using bson_single_pass_sink = conjunction<is_detected<sink_written_t, OutputSinkType>, std::integral_constant<bool, bson_integers_fit_64_bits>>;
/// write a placeholder for the size of an object or array whose entries
/// follow; @return its position, for @ref close_bson_container
std::size_t write_bson_size_placeholder()
{
const std::size_t pos = oa.written();
// zero in any byte order
const std::array<CharType, sizeof(std::int32_t)> zero{};
oa.write_characters(zero.data(), zero.size());
return pos;
}
/// end the object or array whose size is at @a pos and write its size
/// there: everything from that field to the terminating null byte
/// @return false (and the size is not written) if it does not fit
bool close_bson_container(const std::size_t pos)
{
oa.write_character(to_char_type(0x00));
const std::size_t size = oa.written() - pos;
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
{
return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
}
const auto le_size = static_cast<std::uint32_t>(size);
oa.overwrite(pos, is_little_endian ? le_size : byteswap(le_size));
return true;
}
/*!
@brief check the value @a j of a BSON entry as @ref calc_bson_value_size
does, for @ref write_bson_document_single_pass
@return at least the size of the value
A number, boolean, or null needs no check there (its integers have at most
64 bits) and has at most 8 characters; a discarded value is rejected with
the same exception when it is written.
*/
std::size_t check_bson_value(const BasicJsonType& j)
{
return (j.is_string() || j.is_binary()) ? calc_bson_value_size(j) : 8;
}
/// whether an entry of @a entry_size characters keeps the document that
/// begins at @a start, closed by @a open_containers null bytes, within
/// the 32-bit size of BSON
bool bson_entry_fits(const std::size_t start, const std::size_t entry_size, const std::size_t open_containers) const noexcept
{
const auto limit = static_cast<std::size_t>((std::numeric_limits<std::int32_t>::max)());
const std::size_t written = oa.written() - start;
return written <= limit && entry_size <= limit - written && open_containers <= limit - written - entry_size;
}
/*!
@brief Serializes the JSON object @a document as a BSON document in a
single pass, writing the size of each object and array once it is
complete
Checks every key and value as @ref calc_bson_sizes does, in the same
order, so the same exception is thrown; the output written up to then is
discarded with the vector it was written to.
@return false as soon as the document or a string, binary value, object,
or array in it would be larger than the 32-bit BSON sizes allow;
nothing large has been written then, and the caller writes the
document in two passes, which reports that error
*/
template<typename SinkType = OutputSinkType, typename std::enable_if<is_detected<sink_written_t, SinkType>::value, int>::type = 0>
bool write_bson_document_single_pass(const BasicJsonType& document)
{
const std::size_t start = oa.written();
// the object or array whose entries are being written (with the
// position of its size), and the ones it is in
bson_frame current(&document, write_bson_size_placeholder());
std::vector<bson_frame> parents;
while (true)
{
const BasicJsonType* nested = nullptr;
std::size_t nested_size_pos = 0;
if (current.value->is_object())
{
const auto& object = *current.value->m_data.m_value.object;
while (nested == nullptr && current.member != object.cend())
{
const auto& el = *current.member;
++current.member;
const std::size_t header_size = calc_bson_entry_header_size(el.first, el.second);
const std::size_t value_size = el.second.is_structured() ? 0 : check_bson_value(el.second);
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, header_size + value_size, parents.size() + 1)))
{
return false;
}
if (el.second.is_structured())
{
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
nested_size_pos = write_bson_size_placeholder();
nested = &el.second;
}
else
{
write_bson_value(el.first, el.second);
}
}
}
else
{
const auto& array = *current.value->m_data.m_value.array;
while (nested == nullptr && current.index < array.size())
{
if (is_bson_fixed_size(array[current.index]))
{
write_bson_array_fixed_entries(current);
if (current.index == array.size())
{
break;
}
}
// a string, binary value, object or array
const BasicJsonType& el = array[current.index];
const string_t index_name(current.index_name.data(), current.index_name_size);
current.next_index();
const std::size_t value_size = el.is_structured() ? 0 : check_bson_value(el);
if (JSON_HEDLEY_UNLIKELY(!bson_entry_fits(start, 2 + index_name.size() + value_size, parents.size() + 1)))
{
return false;
}
if (el.is_structured())
{
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
nested_size_pos = write_bson_size_placeholder();
nested = &el;
}
else
{
write_bson_value(index_name, el);
}
}
}
if (nested != nullptr)
{
if (nested->is_array())
{
// an array of booleans, numbers and null is written without a frame on the stack
bson_frame leaf(nested, nested_size_pos);
write_bson_array_fixed_entries(leaf);
if (leaf.index == nested->m_data.m_value.array->size())
{
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(nested_size_pos)))
{
return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
}
continue;
}
parents.push_back(std::move(current));
current = std::move(leaf);
continue;
}
parents.push_back(std::move(current));
current = bson_frame(nested, nested_size_pos);
continue;
}
if (JSON_HEDLEY_UNLIKELY(!close_bson_container(current.size_slot)))
{
return false; // LCOV_EXCL_LINE (a document of 2 GiB or more)
}
if (parents.empty())
{
return true;
}
current = std::move(parents.back());
parents.pop_back();
}
}
/*! /*!
@brief Serializes the JSON object @a document as a BSON document @brief Serializes the JSON object @a document as a BSON document
@@ -2350,6 +2544,39 @@ class binary_writer
@pre document.type() == value_t::object @pre document.type() == value_t::object
*/ */
void write_bson_document(const BasicJsonType& document) void write_bson_document(const BasicJsonType& document)
{
write_bson_document(document, bson_single_pass_sink());
}
/// the sink can overwrite: one pass, unless the error handler replaces or
/// drops ill-formed UTF-8, which changes the sizes the checks compute
void write_bson_document(const BasicJsonType& document, std::true_type /*single_pass*/)
{
const std::size_t start = oa.written();
if ((error_handler == error_handler_t::keep || error_handler == error_handler_t::strict)
&& write_bson_document_single_pass(document))
{
return;
}
// a length does not fit: start over, so that it is reported as the
// two passes report it, before anything is written
oa.rewind(start);
write_bson_document_two_pass(document);
}
void write_bson_document(const BasicJsonType& document, std::false_type /*single_pass*/)
{
write_bson_document_two_pass(document);
}
/*!
@brief Serializes the JSON object @a document as a BSON document: the
sizes of all objects and arrays first, then the entries
@param[in] document the JSON object to serialize
@pre document.type() == value_t::object
*/
void write_bson_document_two_pass(const BasicJsonType& document)
{ {
std::vector<std::size_t> nested_sizes; std::vector<std::size_t> nested_sizes;
const std::size_t document_size = calc_bson_sizes(document, nested_sizes); const std::size_t document_size = calc_bson_sizes(document, nested_sizes);
@@ -2552,10 +2779,10 @@ class binary_writer
Selected through masks rather than branches, as the type and the sign of Selected through masks rather than branches, as the type and the sign of
the values in an array of mixed signs alternate unpredictably (non-negative the values in an array of mixed signs alternate unpredictably (non-negative
integers are number_unsigned after parsing). Up to 64 bits, the sign is bit integers are number_unsigned after parsing). The sign is bit 63 of the
63 of the result (a narrower integer is sign-extended); computing it with result (a narrower integer is sign-extended); computing it with masks keeps
masks keeps GCC from branching on the type. A wider integer has to be GCC from branching on the type. Only called if @ref integers_fit_64_bits();
compared. a wider number type is written with range checks on the whole value.
*/ */
static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept static std::uint64_t integer_bits(const BasicJsonType& j, std::uint64_t& sign_mask) noexcept
{ {
@@ -2563,9 +2790,7 @@ class binary_writer
const std::uint64_t raw = is_signed const std::uint64_t raw = is_signed
? static_cast<std::uint64_t>(j.m_data.m_value.number_integer) ? static_cast<std::uint64_t>(j.m_data.m_value.number_integer)
: static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned); : static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned);
sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t) sign_mask = (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u));
? (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u))
: std::uint64_t{0} - static_cast<std::uint64_t>(is_signed && j.m_data.m_value.number_integer < 0);
return raw; return raw;
} }
@@ -3080,6 +3305,25 @@ class binary_writer
} }
} }
/// write the head and the characters of @a value, at most
/// max_text_in_reservation of them, in one reservation
JSON_HEDLEY_ALWAYS_INLINE void write_cbor_string_reserved(const string_t& value)
{
CharType* const out = oa.reserve(value.size() + 2);
CharType* p = out;
if (value.size() <= 0x17)
{
*p++ = to_char_type(static_cast<std::uint8_t>(0x60 + value.size()));
}
else
{
*p++ = to_char_type(0x78);
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
}
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
oa.commit(static_cast<std::size_t>(p - out) + value.size());
}
/*! /*!
@brief write a CBOR text string @brief write a CBOR text string
@@ -3101,19 +3345,7 @@ class binary_writer
// the string is written as it is: head and characters in one reservation // the string is written as it is: head and characters in one reservation
if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation)) if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation))
{ {
CharType* const out = oa.reserve(value.size() + 2); write_cbor_string_reserved(value);
CharType* p = out;
if (value.size() <= 0x17)
{
*p++ = to_char_type(static_cast<std::uint8_t>(0x60 + value.size()));
}
else
{
*p++ = to_char_type(0x78);
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
}
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
oa.commit(static_cast<std::size_t>(p - out) + value.size());
return; return;
} }
} }
@@ -3127,6 +3359,13 @@ class binary_writer
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
void write_cbor_checked_string(const string_t& value, const BasicJsonType& context) void write_cbor_checked_string(const string_t& value, const BasicJsonType& context)
{ {
if (error_handler == error_handler_t::strict && value.size() <= max_text_in_reservation)
{
check_utf8(value, context);
write_cbor_string_reserved(value);
return;
}
string_t storage; string_t storage;
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
@@ -3141,6 +3380,27 @@ class binary_writer
// MsgPack // // MsgPack //
///////////// /////////////
/// write the head and the characters of @a value, at most
/// max_text_in_reservation of them, in one reservation
JSON_HEDLEY_ALWAYS_INLINE void write_msgpack_string_reserved(const string_t& value)
{
CharType* const out = oa.reserve(value.size() + 2);
CharType* p = out;
if (value.size() <= 31)
{
// fixstr
*p++ = to_char_type(static_cast<std::uint8_t>(0xA0 | value.size()));
}
else
{
// str 8
*p++ = to_char_type(0xD9);
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
}
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
oa.commit(static_cast<std::size_t>(p - out) + value.size());
}
/*! /*!
@brief write a MessagePack str @brief write a MessagePack str
@@ -3158,21 +3418,7 @@ class binary_writer
// the string is written as it is: head and characters in one reservation // the string is written as it is: head and characters in one reservation
if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation)) if (JSON_HEDLEY_LIKELY(value.size() <= max_text_in_reservation))
{ {
CharType* const out = oa.reserve(value.size() + 2); write_msgpack_string_reserved(value);
CharType* p = out;
if (value.size() <= 31)
{
// fixstr
*p++ = to_char_type(static_cast<std::uint8_t>(0xA0 | value.size()));
}
else
{
// str 8
*p++ = to_char_type(0xD9);
*p++ = to_char_type(static_cast<std::uint8_t>(value.size()));
}
copy_characters(p, reinterpret_cast<const CharType*>(value.data()), value.size());
oa.commit(static_cast<std::size_t>(p - out) + value.size());
return; return;
} }
} }
@@ -3185,6 +3431,13 @@ class binary_writer
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
void write_msgpack_checked_string(const string_t& value, const BasicJsonType& context) void write_msgpack_checked_string(const string_t& value, const BasicJsonType& context)
{ {
if (error_handler == error_handler_t::strict && value.size() <= max_text_in_reservation)
{
check_utf8(value, context);
write_msgpack_string_reserved(value);
return;
}
string_t storage; string_t storage;
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
@@ -3904,57 +4157,34 @@ class binary_writer
} }
/*! /*!
@brief validate (dry_run) or write one BJData ND-array element of integer dtype @a T @brief whether the BJData ND-array element @a el can be written with dtype 'd' (single precision)
@return whether @a el's value is in range of @a T; always true when @a dry_run is false
*/
template<typename T>
bool write_bjdata_ndarray_element(const BasicJsonType& el, const bool dry_run)
{
if (dry_run)
{
return bjdata_ndarray_value_in_range<T>(el);
}
using storage_type = typename std::conditional<std::is_unsigned<T>::value, std::uint64_t, std::int64_t>::type;
write_number(static_cast<T>(el.template get<storage_type>()), true);
return true;
}
/*!
@brief validate (dry_run) or write one BJData ND-array element of dtype 'd' (single precision)
@return whether @a el's value survives narrowing to float and back without any change @return whether @a el's value survives narrowing to float and back without any change
(so the ND-array round-trips exactly), or is infinite or NaN; always true when (so the ND-array round-trips exactly), or is infinite or NaN
@a dry_run is false
*/ */
bool write_bjdata_ndarray_float_element(const BasicJsonType& el, const bool dry_run) static bool bjdata_ndarray_float_in_range(const BasicJsonType& el) noexcept
{ {
const auto dval = static_cast<double>(el.m_data.m_value.number_float); const auto dval = static_cast<double>(el.m_data.m_value.number_float);
if (dry_run)
{
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
#endif #endif
// a value that would be rounded (rather than exactly represented) by the // a value that would be rounded (rather than exactly represented) by the
// narrowing to float is treated like an out-of-range integer element; this // narrowing to float is treated like an out-of-range integer element; this
// is the same criterion write_compact_float() uses for CBOR/MessagePack // is the same criterion write_compact_float() uses for CBOR/MessagePack
const bool in_range = std::isnan(dval) || return std::isnan(dval) ||
(dval >= static_cast<double>(std::numeric_limits<float>::lowest()) && (dval >= static_cast<double>(std::numeric_limits<float>::lowest()) &&
dval <= static_cast<double>((std::numeric_limits<float>::max)()) && dval <= static_cast<double>((std::numeric_limits<float>::max)()) &&
static_cast<double>(static_cast<float>(dval)) == dval) || static_cast<double>(static_cast<float>(dval)) == dval) ||
std::isinf(dval); std::isinf(dval);
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_POP JSON_HEDLEY_DIAGNOSTIC_POP
#endif #endif
return in_range;
}
write_number(static_cast<float>(dval), true);
return true;
} }
/*! /*!
@brief write the elements of an ND-array as the number kind @a dtype names @brief write the elements of an ND-array as the number kind @a dtype names
(see @ref write_bjdata_ndarray), after they have been found to be (see @ref write_bjdata_ndarray), after they have been found to be
in range by a dry run of @ref write_bjdata_ndarray_elements in range by @ref bjdata_ndarray_elements_in_range
The elements are encoded in blocks into space reserved from the sink. The elements are encoded in blocks into space reserved from the sink.
*/ */
@@ -4036,20 +4266,13 @@ class binary_writer
} }
/*! /*!
@brief validate or write every element of a BJData ND-array's `_ArrayData_` @brief whether every element of a BJData ND-array's `_ArrayData_` is in range for its dtype
@param[in] array_data the `_ArrayData_` array @param[in] array_data the `_ArrayData_` array
@param[in] dtype the ND-array dtype marker, as returned by bjdata_ndarray_type_marker() @param[in] dtype the ND-array dtype marker, as returned by bjdata_ndarray_type_marker()
@param[in] dry_run true to only range-check each element, false to write it @return whether every element is in range for @a dtype
@return whether every element is in range for @a dtype (always true when @a dry_run is false)
*/ */
bool write_bjdata_ndarray_elements(const BasicJsonType& array_data, const CharType dtype, const bool dry_run) static bool bjdata_ndarray_elements_in_range(const BasicJsonType& array_data, const CharType dtype)
{ {
if (!dry_run)
{
write_bjdata_ndarray_values(array_data, dtype);
return true;
}
for (const auto& el : array_data) for (const auto& el : array_data)
{ {
bool ok = true; bool ok = true;
@@ -4057,43 +4280,39 @@ class binary_writer
{ {
case 'U': case 'U':
case 'B': case 'B':
ok = write_bjdata_ndarray_element<std::uint8_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint8_t>(el);
break; break;
case 'C': case 'C':
// BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges) // BJData limits a char to 0..127 (the intersection of the int8 and uint8 ranges)
ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el); ok = bjdata_ndarray_value_in_range<std::int8_t>(el) && bjdata_ndarray_value_in_range<std::uint8_t>(el);
break; break;
case 'i': case 'i':
ok = write_bjdata_ndarray_element<std::int8_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int8_t>(el);
break; break;
case 'u': case 'u':
ok = write_bjdata_ndarray_element<std::uint16_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint16_t>(el);
break; break;
case 'I': case 'I':
ok = write_bjdata_ndarray_element<std::int16_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int16_t>(el);
break; break;
case 'm': case 'm':
ok = write_bjdata_ndarray_element<std::uint32_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint32_t>(el);
break; break;
case 'l': case 'l':
ok = write_bjdata_ndarray_element<std::int32_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int32_t>(el);
break; break;
case 'M': case 'M':
ok = write_bjdata_ndarray_element<std::uint64_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::uint64_t>(el);
break; break;
case 'L': case 'L':
ok = write_bjdata_ndarray_element<std::int64_t>(el, dry_run); ok = bjdata_ndarray_value_in_range<std::int64_t>(el);
break; break;
case 'd': case 'd':
ok = write_bjdata_ndarray_float_element(el, dry_run); ok = bjdata_ndarray_float_in_range(el);
break; break;
case 'D': case 'D':
default: default:
// 'D' (double) already spans the full range of number_float_t // 'D' (double) already spans the full range of number_float_t
if (!dry_run)
{
write_number(el.template get<double>(), true);
}
break; break;
} }
if (!ok) if (!ok)
@@ -4180,7 +4399,7 @@ class binary_writer
const auto dim = el.template get<std::uint64_t>(); const auto dim = el.template get<std::uint64_t>();
if (!value_in_range_of<std::size_t>(dim)) if (!value_in_range_of<std::size_t>(dim))
{ {
return true; return true; // LCOV_EXCL_LINE (size_t is narrower than 64 bits)
} }
const auto dim_size = static_cast<std::size_t>(dim); const auto dim_size = static_cast<std::size_t>(dim);
@@ -4230,7 +4449,7 @@ class binary_writer
// wrap (integers) or overflow to infinity (the "single" precision // wrap (integers) or overflow to infinity (the "single" precision
// float) instead of being reported, so such an object falls back to // float) instead of being reported, so such an object falls back to
// a plain object encoding as well // a plain object encoding as well
if (!write_bjdata_ndarray_elements(array_data, dtype, true)) if (!bjdata_ndarray_elements_in_range(array_data, dtype))
{ {
return true; return true;
} }
@@ -4242,7 +4461,7 @@ class binary_writer
write_ubjson(array_size, use_count, use_type, true, true, bjdata_version); write_ubjson(array_size, use_count, use_type, true, true, bjdata_version);
write_bjdata_ndarray_elements(array_data, dtype, false); write_bjdata_ndarray_values(array_data, dtype);
return false; return false;
} }
@@ -4568,8 +4787,8 @@ class binary_writer
run are encoded into space reserved from the sink, as in run are encoded into space reserved from the sink, as in
@ref write_array_elements; an integer that BON8 cannot write (an unsigned @ref write_array_elements; an integer that BON8 cannot write (an unsigned
one above the int64 range, or one of a number type wider than 64 bits) throws one above the int64 range, or one of a number type wider than 64 bits) throws
from write_bon8_value, like any other value that is not a number. Kept out out_of_range.407, as write_bon8_value does. Kept out of line, like
of line, like @ref write_cbor_array. @ref write_cbor_array.
*/ */
template<typename ArrayType> template<typename ArrayType>
JSON_HEDLEY_NEVER_INLINE JSON_HEDLEY_NEVER_INLINE
@@ -4619,10 +4838,10 @@ class binary_writer
string_open = false; string_open = false;
} }
if (unencodable) if (JSON_HEDLEY_UNLIKELY(unencodable))
{ {
write_bon8_value(*it, string_open, depth + 1); // an integer beyond the int64 range, as write_bon8_value would report it
++it; throw_integer_out_of_range(*it, input_format_t::bon8, "int64");
} }
} }
} }
@@ -4657,14 +4876,13 @@ class binary_writer
break; break;
case value_t::number_integer: case value_t::number_integer:
if (JSON_HEDLEY_LIKELY(integer_fits<std::int64_t>(value.m_data.m_value.number_integer))) if (JSON_HEDLEY_UNLIKELY(!integer_fits<std::int64_t>(value.m_data.m_value.number_integer)))
{ {
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_integer)); // beyond 64 bits (number types wider than that)
string_open = false; throw_integer_out_of_range(value, input_format_t::bon8, "int64");
break;
} }
// beyond 64 bits (number types wider than that): throws write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_integer));
write_bon8_value(value, string_open, depth + 1); string_open = false;
break; break;
case value_t::number_float: case value_t::number_float:
@@ -4673,14 +4891,13 @@ class binary_writer
break; break;
case value_t::number_unsigned: case value_t::number_unsigned:
if (JSON_HEDLEY_LIKELY(integer_fits_max<std::int64_t>(value.m_data.m_value.number_unsigned))) if (JSON_HEDLEY_UNLIKELY(!integer_fits_max<std::int64_t>(value.m_data.m_value.number_unsigned)))
{ {
write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_unsigned)); // too large for BON8
string_open = false; throw_integer_out_of_range(value, input_format_t::bon8, "int64");
break;
} }
// too large for BON8: throws write_bon8_integer(static_cast<std::int64_t>(value.m_data.m_value.number_unsigned));
write_bon8_value(value, string_open, depth + 1); string_open = false;
break; break;
case value_t::object: case value_t::object:
@@ -4794,10 +5011,9 @@ class binary_writer
static void check_utf8(const string_t& s, const BasicJsonType& context) static void check_utf8(const string_t& s, const BasicJsonType& context)
{ {
const auto* data = reinterpret_cast<const unsigned char*>(s.data()); const auto* data = reinterpret_cast<const unsigned char*>(s.data());
const std::size_t valid = valid_utf8_prefix(data, s.size()); if (JSON_HEDLEY_UNLIKELY(!valid_utf8_string(data, s.size())))
if (JSON_HEDLEY_UNLIKELY(valid != s.size()))
{ {
throw_invalid_utf8(data, valid, context); throw_invalid_utf8(data, valid_utf8_prefix(data, s.size()), context);
} }
} }
@@ -11,6 +11,7 @@
#include <algorithm> // max #include <algorithm> // max
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t
#include <cstring> // memcpy #include <cstring> // memcpy
#include <memory> // shared_ptr, make_shared #include <memory> // shared_ptr, make_shared
#include <string> // basic_string #include <string> // basic_string
@@ -227,6 +228,31 @@ class output_vector_sink
void flush() noexcept void flush() noexcept
{} {}
/// the number of characters written so far, the position of the next one
std::size_t written() const noexcept
{
return position();
}
/// @brief replace the 4 bytes written before at position @a pos with
/// those of @a n, in the byte order of the machine
///
/// Lets a writer put a length in front of data it has written since, such
/// as the size of a BSON document, without computing it in advance.
void overwrite(const std::size_t pos, const std::uint32_t n) noexcept
{
static_assert(sizeof(CharType) == 1, "overwrite() writes bytes");
JSON_ASSERT(pos + sizeof(n) <= position());
std::memcpy(v->data() + pos, &n, sizeof(n));
}
/// drop the characters written from position @a pos on
void rewind(const std::size_t pos) noexcept
{
JSON_ASSERT(pos <= position());
cursor = v->data() + pos;
}
private: private:
/// number of characters of *v written so far /// number of characters of *v written so far
std::size_t position() const noexcept std::size_t position() const noexcept
+42 -23
View File
@@ -1113,36 +1113,21 @@ class serializer
{ {
// the code point is at least 0x60 (smaller ones are handled // the code point is at least 0x60 (smaller ones are handled
// above) and is not printable ASCII (which the scanner // above) and is not printable ASCII (which the scanner
// copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII // copies). Under EnsureAscii it is DEL (0x7F): every
// character // well-formed multi-byte character is escaped by
// write_u_escapes() above, so decode() only ever accepts a
// single byte here, and nothing of it was buffered
if (EnsureAscii) if (EnsureAscii)
{ {
JSON_ASSERT(codepoint >= 0x7F); JSON_ASSERT(codepoint == 0x7F);
if (error_handler == error_handler_t::keep) JSON_ASSERT(bytes == bytes_after_last_accept);
{ write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
// 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 else
{ {
// copy byte to buffer (all previous bytes of the // copy byte to buffer (all previous bytes of the
// sequence have been copied in the default case below) // sequence have been copied in the default case below)
string_buffer[bytes++] = s[i]; string_buffer[bytes++] = s[i]; // LCOV_EXCL_LINE (well-formed sequences are copied in runs)
} }
reserve_string_buffer(bytes); reserve_string_buffer(bytes);
@@ -1977,6 +1962,23 @@ class serializer
return; return;
} }
// 17 to 20 digits: the first one to four, then twice eight
if (magnitude_fits_64_bits(abs_value, std::integral_constant<bool, magnitude_has_at_most_64_bits>()))
{
const auto value = static_cast<std::uint64_t>(abs_value);
const std::uint64_t top = value / 10000000000000000u; // 1 to 1844
const std::uint64_t low16 = value - (top * 10000000000000000u);
const std::uint64_t middle = low16 / 100000000u;
const std::uint64_t first = dtoa_impl::eight_digit_bytes(top);
const auto leading = static_cast<unsigned>(count_leading_zeros(first) / 8); // (first is not 0)
char* const p = buffer_ptr + n_chars;
dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u);
dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u);
dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u);
write_buffer_pos += n_chars + 24 - leading;
return;
}
n_chars += count_digits(abs_value); n_chars += count_digits(abs_value);
// spare 1 byte for '\0' // spare 1 byte for '\0'
@@ -2122,6 +2124,23 @@ class serializer
} }
} }
/// whether every number_magnitude_t fits 64 bits
static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits<number_magnitude_t>::is_specialized
&& std::numeric_limits<number_magnitude_t>::digits <= 64;
/// whether @a x fits 64 bits: always if number_magnitude_t has at most
/// 64 bits (selected by type, as the comparison would be reported as
/// always true), else compared
static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept
{
return true;
}
static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept
{
return x <= 18446744073709551615u;
}
/* /*
* Helper function for dump_integer * Helper function for dump_integer
* *
+1 -1
View File
@@ -139,7 +139,7 @@ inline OutStringType concat(Args && ... args)
str.reserve(concat_length(args...)); str.reserve(concat_length(args...));
concat_into(str, std::forward<Args>(args)...); concat_into(str, std::forward<Args>(args)...);
return str; return str;
} } // LCOV_EXCL_LINE (exception cleanup only)
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
@@ -83,10 +83,6 @@ template<typename StringType>
inline void unescape(StringType& s) inline void unescape(StringType& s)
{ {
auto pos = s.find_first_of('~', 0); auto pos = s.find_first_of('~', 0);
if (pos == StringType::npos)
{
return;
}
StringType result; StringType result;
result.reserve(s.size()); result.reserve(s.size());
+3 -3
View File
@@ -36,7 +36,7 @@ StringType to_string(std::size_t value)
StringType result; StringType result;
int_to_string(result, value); int_to_string(result, value);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @return a byte as two uppercase hexadecimal digits /// @return a byte as two uppercase hexadecimal digits
inline std::string hex_byte(const std::uint8_t byte) inline std::string hex_byte(const std::uint8_t byte)
@@ -46,7 +46,7 @@ inline std::string hex_byte(const std::uint8_t byte)
result[0] = nibble_to_hex[byte / 16]; result[0] = nibble_to_hex[byte / 16];
result[1] = nibble_to_hex[byte % 16]; result[1] = nibble_to_hex[byte % 16];
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/*! /*!
@brief write a UTF-16 code unit as four lowercase hexadecimal digits @brief write a UTF-16 code unit as four lowercase hexadecimal digits
@@ -390,7 +390,7 @@ inline StringType sanitize_utf8(const StringType& s, const error_handler_t error
} }
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
+138 -20
View File
@@ -69,7 +69,8 @@ struct parse_failure
}; };
/// FloatType: the number_float_t of the document, whose overflow parse() rejects /// FloatType: the number_float_t of the document, whose overflow parse() rejects
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Sentinel> /// Store: false for accept(): the grammar, number, string and UTF-8 checks run as for parse(), but no node is written
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Sentinel, bool Store>
class builder class builder
{ {
public: public:
@@ -245,11 +246,87 @@ class builder
return p; return p;
} }
/// the string from p, the first backslash, control character, ill-formed UTF-8 byte, or the end, checked and not
/// decoded (accept() stores nothing, so the arena is not written): the checks, errors and error positions of
/// slow_string(), for everything up to the closing quote
NLOHMANN_VIEW_NOINLINE decoded check_string(const unsigned char* p)
{
const unsigned char* const end = m_end;
for (;;)
{
if (p == end)
{
return failed(error_code::string_missing_quote, p);
}
const unsigned char c = *p;
if (c == '"')
{
++p;
return decoded{p, 0, 0};
}
if (c != '\\')
{
return failed(c < 0x20 ? error_code::string_control_character : error_code::string_utf8, p);
}
++p;
if (p == end)
{
return failed(error_code::string_missing_quote, p);
}
const unsigned char d = *p++;
if (d == 'u')
{
const int cp = hex4(p); // (moves p past the four digits)
if (NLOHMANN_VIEW_UNLIKELY(cp < 0))
{
return failed(error_code::string_unicode_hex, p);
}
if (NLOHMANN_VIEW_UNLIKELY((cp & 0xF800) == 0xD800)) // a surrogate
{
if (cp >= 0xDC00)
{
return failed(error_code::string_surrogate_low, p);
}
if (end - p < 2 || p[0] != '\\' || p[1] != 'u')
{
return failed(error_code::string_surrogate_high, p);
}
p += 2;
const int lo = hex4(p); // (moves p past the four digits)
if (lo < 0)
{
return failed(error_code::string_unicode_hex, p);
}
if (lo < 0xDC00 || lo > 0xDFFF)
{
return failed(error_code::string_surrogate_high, p);
}
}
}
else if (!(d == '"' || d == '\\' || d == '/' || d == 'b' || d == 'f' || d == 'n' || d == 'r' || d == 't'))
{
--p;
return failed(error_code::string_escape, p);
}
if (p != end && *p == '\\')
{
continue; // (escapes in a row: no run of text between them)
}
p = scan_string_run(p, end); // the text up to the next backslash, quote, control character, or ill-formed UTF-8
}
}
/// s points to the start of the string, p to a backslash, a control /// s points to the start of the string, p to a backslash, a control
/// character, an ill-formed UTF-8 byte, or the end: decode the string into /// character, an ill-formed UTF-8 byte, or the end: decode the string into
/// the arena /// the arena
NLOHMANN_VIEW_NOINLINE decoded slow_string(const unsigned char* s, const unsigned char* p) NLOHMANN_VIEW_NOINLINE decoded slow_string(const unsigned char* s, const unsigned char* p)
{ {
if (!enabled(Store))
{
// (accept(): the string is checked, not decoded)
static_cast<void>(s);
return check_string(p);
}
// single-character escapes; 0: invalid (and 'u', handled separately) // single-character escapes; 0: invalid (and 'u', handled separately)
static const std::array<char, 128> simple_escape = static const std::array<char, 128> simple_escape =
{ {
@@ -343,6 +420,21 @@ class builder
} }
arena_ensure(w, w_end, 4); arena_ensure(w, w_end, 4);
w = put_utf8(w, cp); w = put_utf8(w, cp);
// A chain of unicode escapes ("\u3042\u3044\u3046", as Python's json.dumps writes non-ASCII text) is
// decoded here, without going round the loop above for each of them. Anything else (a surrogate, a
// bad digit, fewer than six bytes) leaves p at the backslash: the loop above handles it, and reports
// the same error at the same position as it would without this loop.
while (end - p >= 6 && p[0] == '\\' && p[1] == 'u')
{
const int next = hex_codepoint(p + 2);
if (next < 0 || (next & 0xF800) == 0xD800)
{
break;
}
p += 6;
arena_ensure(w, w_end, 4);
w = put_utf8(w, next);
}
} }
else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0))
{ {
@@ -610,10 +702,13 @@ run_found:
NLOHMANN_VIEW_ALWAYS_INLINE bool run() NLOHMANN_VIEW_ALWAYS_INLINE bool run()
{ {
cold.m_doc.reserve(estimate_nodes(reinterpret_cast<const char*>(b), static_cast<std::size_t>(e - b))); if (enabled(Store))
base = cold.m_doc.nodes; {
out = base; cold.m_doc.reserve(estimate_nodes(reinterpret_cast<const char*>(b), static_cast<std::size_t>(e - b)));
cap = base + cold.m_doc.node_capacity; base = cold.m_doc.nodes;
out = base;
cap = base + cold.m_doc.node_capacity;
}
if (e - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) if (e - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF)
{ {
@@ -797,7 +892,10 @@ close_object:
// an array pays nothing for this) // an array pays nothing for this)
if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members)) if (NLOHMANN_VIEW_UNLIKELY(cur_count >= document_data::index_min_members))
{ {
cold.note_large_object(cur_idx); if (enabled(Store))
{
cold.note_large_object(cur_idx);
}
} }
close_container: close_container:
@@ -957,6 +1055,12 @@ indent_done:
/// an integer's value); two stores on little-endian targets /// an integer's value); two stores on little-endian targets
NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second) NLOHMANN_VIEW_ALWAYS_INLINE node* push_node(node_kind k, std::uint8_t flags, std::uint16_t extra, std::size_t off, std::uint64_t second)
{ {
if (!enabled(Store))
{
// accept(): nothing is stored, so what computes the arguments is not run either. base and out stay
// null: their difference, which open() takes, is 0.
return out;
}
if (NLOHMANN_VIEW_UNLIKELY(out == cap)) if (NLOHMANN_VIEW_UNLIKELY(out == cap))
{ {
const auto n = static_cast<std::size_t>(out - base); const auto n = static_cast<std::size_t>(out - base);
@@ -1015,9 +1119,12 @@ indent_done:
NLOHMANN_VIEW_ALWAYS_INLINE void close() NLOHMANN_VIEW_ALWAYS_INLINE void close()
{ {
node& n = base[cur_idx]; if (enabled(Store))
n.len = cur_count; {
n.next = static_cast<std::uint32_t>(out - base) - cur_idx; node& n = base[cur_idx];
n.len = cur_count;
n.next = static_cast<std::uint32_t>(out - base) - cur_idx;
}
if (--depth != 0) if (--depth != 0)
{ {
if (NLOHMANN_VIEW_LIKELY(depth <= 64)) if (NLOHMANN_VIEW_LIKELY(depth <= 64))
@@ -1067,8 +1174,9 @@ indent_done:
{ {
++p; ++p;
} }
else if (NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9')) else if (!negative || NLOHMANN_VIEW_LIKELY(cur() >= '1' && cur() <= '9'))
{ {
// (without a sign, the dispatch saw a digit)
p = skip_digits(p + 1, e); p = skip_digits(p + 1, e);
} }
else else
@@ -1082,7 +1190,7 @@ indent_done:
{ {
++p; ++p;
const unsigned char* const f0 = p; const unsigned char* const f0 = p;
p = skip_digits(p, e); p = skip_fraction_digits(p, e);
if (NLOHMANN_VIEW_UNLIKELY(p == f0)) if (NLOHMANN_VIEW_UNLIKELY(p == f0))
{ {
return fail(error_code::number_after_dot); return fail(error_code::number_after_dot);
@@ -1192,34 +1300,44 @@ indent_done:
}; };
/// run the builder with compile-time options /// run the builder with compile-time options
template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd> template<typename FloatType, bool Comments, bool TrailingCommas, bool NulIsEnd, bool Store>
inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure) inline bool build_with(document_data& d, const char* src, std::size_t size, bool sentinel, parse_failure& failure)
{ {
if (sentinel) if (sentinel)
{ {
builder<FloatType, Comments, TrailingCommas, NulIsEnd, true> bld(d, src, size); builder<FloatType, Comments, TrailingCommas, NulIsEnd, true, Store> bld(d, src, size);
const bool ok = bld.run(); const bool ok = bld.run();
failure = bld.failure(); failure = bld.failure();
return ok; return ok;
} }
builder<FloatType, Comments, TrailingCommas, NulIsEnd, false> bld(d, src, size); builder<FloatType, Comments, TrailingCommas, NulIsEnd, false, Store> bld(d, src, size);
const bool ok = bld.run(); const bool ok = bld.run();
failure = bld.failure(); failure = bld.failure();
return ok; return ok;
} }
/// sentinel: src[size] is readable and 0 (e.g. std::string); FloatType: the /// sentinel: src[size] is readable and 0 (e.g. std::string); FloatType: the
/// number_float_t of the document /// number_float_t of the document; validate_only: accept(), only the result is
/// used
template<typename FloatType, bool NulIsEnd> template<typename FloatType, bool NulIsEnd>
inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, parse_failure& failure) inline bool build(document_data& d, const char* src, std::size_t size, bool comments, bool trailing_commas, bool sentinel, bool validate_only,
parse_failure& failure)
{ {
if (validate_only && !comments && !trailing_commas)
{
// accept() of plain JSON: nothing is stored. The other option sets
// could skip the stores just as well; they keep the builder that
// stores only so that there are two more instantiations of the parse
// loop, not eight.
return build_with<FloatType, false, false, NulIsEnd, false>(d, src, size, sentinel, failure);
}
if (comments) if (comments)
{ {
return trailing_commas ? build_with<FloatType, true, true, NulIsEnd>(d, src, size, sentinel, failure) return trailing_commas ? build_with<FloatType, true, true, NulIsEnd, true>(d, src, size, sentinel, failure)
: build_with<FloatType, true, false, NulIsEnd>(d, src, size, sentinel, failure); : build_with<FloatType, true, false, NulIsEnd, true>(d, src, size, sentinel, failure);
} }
return trailing_commas ? build_with<FloatType, false, true, NulIsEnd>(d, src, size, sentinel, failure) return trailing_commas ? build_with<FloatType, false, true, NulIsEnd, true>(d, src, size, sentinel, failure)
: build_with<FloatType, false, false, NulIsEnd>(d, src, size, sentinel, failure); : build_with<FloatType, false, false, NulIsEnd, true>(d, src, size, sentinel, failure);
} }
} // namespace view } // namespace view
+13 -22
View File
@@ -397,9 +397,9 @@ class view_side
return m_view.size(); return m_view.size();
} }
string_view_t string() const string_view_t string() const noexcept
{ {
return m_view.get_string(); return {m_view.m_doc->str(*m_view.m_node), m_view.m_node->len};
} }
bool boolean() const noexcept bool boolean() const noexcept
@@ -428,28 +428,19 @@ class view_side
return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len); return string_view_t(m_view.m_doc->str(*m_view.m_node), m_view.m_node->len);
} }
/// a number, boolean, or null as a basic_json value (no allocation) /// a number as a basic_json value (no allocation); only called for numbers
BasicJsonType scalar() const BasicJsonType scalar() const
{ {
switch (m_view.type()) const value_t t = m_view.type();
if (t == value_t::number_integer)
{ {
case value_t::number_integer: return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>());
return BasicJsonType(m_view.template get<typename BasicJsonType::number_integer_t>());
case value_t::number_unsigned:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
case value_t::number_float:
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
case value_t::boolean:
return BasicJsonType(m_view.template get<bool>());
case value_t::null:
case value_t::object:
case value_t::array:
case value_t::string:
case value_t::binary:
case value_t::discarded:
default:
return BasicJsonType(nullptr);
} }
if (t == value_t::number_unsigned)
{
return BasicJsonType(m_view.template get<typename BasicJsonType::number_unsigned_t>());
}
return BasicJsonType(m_view.template get<typename BasicJsonType::number_float_t>());
} }
/// start at the first element of this array /// start at the first element of this array
@@ -531,9 +522,9 @@ class json_side
return m_json->size(); return m_json->size();
} }
string_view_t string() const string_view_t string() const noexcept
{ {
const auto& s = m_json->template get_ref<const typename BasicJsonType::string_t&>(); const auto& s = *m_json->template get_ptr<const typename BasicJsonType::string_t*>();
return string_view_t(s.data(), s.size()); return string_view_t(s.data(), s.size());
} }
+13 -13
View File
@@ -96,11 +96,11 @@ NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::
} }
if (*q >= 0x80) if (*q >= 0x80)
{ {
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p)); break; // ill-formed UTF-8: reported below
return; // not reached: the scan stopped at ill-formed UTF-8
} }
++q; // a quote, backslash, or control character ++q; // a quote, backslash, or control character
} }
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
} }
inline void check_utf8(const char* s, std::size_t n) inline void check_utf8(const char* s, std::size_t n)
@@ -157,7 +157,7 @@ class editor
node* const o = own(object); node* const o = own(object);
if (o->kind != node_kind::object && o->kind != node_kind::null) if (o->kind != node_kind::object && o->kind != node_kind::null)
{ {
throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); throw_type_error(site_of(object), exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name());
} }
check_utf8(key.data(), key.size()); check_utf8(key.data(), key.size());
const encoded e = encode(std::forward<V>(value)); const encoded e = encode(std::forward<V>(value));
@@ -208,9 +208,9 @@ class editor
node* const a = own(array); node* const a = own(array);
if (a->kind != node_kind::array) if (a->kind != node_kind::array)
{ {
throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); throw_type_error(site_of(array), exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name());
} }
check_index(idx, a->len); check_index(a, idx, a->len);
const encoded e = encode(std::forward<V>(value)); const encoded e = encode(std::forward<V>(value));
node* const slot = const_cast<node*>(nav::value(element_at<true>(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast) node* const slot = const_cast<node*>(nav::value(element_at<true>(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast)
assign(slot, e, a, true); assign(slot, e, a, true);
@@ -225,7 +225,7 @@ class editor
node* const a = own(array); node* const a = own(array);
if (a->kind != node_kind::array && a->kind != node_kind::null) if (a->kind != node_kind::array && a->kind != node_kind::null)
{ {
throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); throw_type_error(site_of(array), exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name());
} }
const encoded e = encode(std::forward<V>(value)); const encoded e = encode(std::forward<V>(value));
if (a->kind == node_kind::null) if (a->kind == node_kind::null)
@@ -249,9 +249,9 @@ class editor
node* const a = own(array); node* const a = own(array);
if (a->kind != node_kind::array) if (a->kind != node_kind::array)
{ {
throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); throw_type_error(site_of(array), exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name());
} }
check_index(idx, a->len + 1); check_index(a, idx, a->len + 1);
const encoded e = encode(std::forward<V>(value)); const encoded e = encode(std::forward<V>(value));
node* const slot = new_slot(e); node* const slot = new_slot(e);
node* const h = block_of(m_doc, a, 1); node* const h = block_of(m_doc, a, 1);
@@ -269,7 +269,7 @@ class editor
node* const o = own(object); node* const o = own(object);
if (o->kind != node_kind::object) if (o->kind != node_kind::object)
{ {
throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); throw_type_error(site_of(object), exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name());
} }
for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1))
{ {
@@ -287,9 +287,9 @@ class editor
node* const a = own(array); node* const a = own(array);
if (a->kind != node_kind::array) if (a->kind != node_kind::array)
{ {
throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); throw_type_error(site_of(array), exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name());
} }
check_index(idx, a->len); check_index(a, idx, a->len);
node* const h = block_of(m_doc, a, 0); node* const h = block_of(m_doc, a, 0);
std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node));
--h->next; --h->next;
@@ -316,11 +316,11 @@ class editor
return const_cast<node*>(v.m_node); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document return const_cast<node*>(v.m_node); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document
} }
static void check_index(std::size_t idx, std::size_t limit) void check_index(const node* array, std::size_t idx, std::size_t limit) const
{ {
if (idx >= limit) if (idx >= limit)
{ {
throw_array_index_out_of_range(idx); throw_array_index_out_of_range(error_site(&m_doc, array), idx);
} }
} }
@@ -158,7 +158,7 @@ inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap)
e.moved_cap[n->off] = cap; e.moved_cap[n->off] = cap;
return; return;
} }
reserve_moved(d); // every caller has called reserve_moved() (see above), so neither push_back() can throw
e.moved.push_back(seq); e.moved.push_back(seq);
e.moved_cap.push_back(cap); e.moved_cap.push_back(cap);
n->off = static_cast<std::uint32_t>(e.moved.size() - 1); n->off = static_cast<std::uint32_t>(e.moved.size() - 1);
@@ -243,7 +243,7 @@ inline node* find_parent(const document_data& d, const node* target)
} }
if (target == lo) if (target == lo)
{ {
return nullptr; return nullptr; // LCOV_EXCL_LINE (the root, or a new value's root, which is linked)
} }
for (;;) for (;;)
{ {
+314 -8
View File
@@ -10,11 +10,15 @@
#include <algorithm> // min #include <algorithm> // min
#include <cstddef> // size_t #include <cstddef> // size_t
#include <string> // string #include <string> // string, to_string
#include <vector> // vector
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <nlohmann/detail/view/builder.hpp> #include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/document_data.hpp>
#include <nlohmann/detail/view/macro_scope.hpp> #include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/materialize.hpp>
#include <nlohmann/detail/view/node.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail namespace detail
@@ -22,8 +26,289 @@ namespace detail
namespace view namespace view
{ {
/*!
@brief the value an exception is about
basic_json passes the value to the exception, which then names its path
(JSON_DIAGNOSTICS) and its bytes in the source (JSON_DIAGNOSTIC_POSITIONS). A
view's value has no parent pointer and no stored positions: both are found
from the document and the node, when the exception is thrown (so only the
error path pays for them). Without the two macros, a site is empty.
*/
class error_site
{
public:
error_site() noexcept = default;
#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS
error_site(const document_data* d, const node* n) noexcept
: m_doc(d), m_node(n)
{}
const document_data* doc() const noexcept
{
return m_doc;
}
const node* value() const noexcept
{
return m_node;
}
private:
const document_data* m_doc = nullptr;
const node* m_node = nullptr;
#else
error_site(const document_data* /*d*/, const node* /*n*/) noexcept {}
#endif
};
/// the site of a view's value (the views make this a friend)
struct view_access
{
template<typename View>
static error_site site(const View& v) noexcept
{
return {v.m_doc, v.m_node};
}
};
template<typename View>
error_site site_of(const View& v) noexcept
{
static_assert(is_json_view<View>::value, "the site of a value is only defined for views");
return view_access::site(v);
}
#if JSON_DIAGNOSTICS
/*!
@brief the JSON pointer tokens from the root of document @a d to node @a target
Nodes have no parent index, so the path is found by descending from the root:
before the first edit, only into the subtree that contains the target (a
container spans the next `next` nodes); after edits, elements can live in
other sequences, so every container is searched. Iterative (the nesting depth
is limited by memory only). False if the target is not found (then tokens is
unspecified).
*/
template<bool Editable>
bool value_path(const document_data& d, const node* target, std::vector<std::string>& tokens)
{
using nav = navigation<Editable>;
struct frame
{
const node* pos;
const node* end;
std::size_t index;
bool object;
};
const node* const root = d.nodes;
if (target == root)
{
return true;
}
if (!is_container(*root))
{
return false;
}
std::vector<frame> stack;
stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object});
// (tokens[i] leads from stack[i] to stack[i + 1], or to the target)
for (;;)
{
// a copy: pop_back() and push_back() below change the stack
const frame f = stack.back();
if (f.pos == f.end)
{
stack.pop_back();
if (stack.empty())
{
return false;
}
tokens.pop_back();
continue;
}
const node* entry = f.pos;
std::string token;
if (f.object)
{
// (as basic_json: the key up to a null byte)
token.assign(d.str(*entry), entry->len);
const std::size_t nul = token.find('\0');
if (nul != std::string::npos)
{
token.resize(nul);
}
++entry;
}
else
{
token = std::to_string(f.index);
}
const node* const child = nav::value(entry);
if (child == target)
{
tokens.push_back(token);
return true;
}
stack.back().pos = document_data::after(entry);
++stack.back().index;
if (is_container(*child) && (nav::editable() || (target > child && target < child + child->next)))
{
tokens.push_back(token);
stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object});
}
}
}
#endif
#if JSON_DIAGNOSTIC_POSITIONS
/*!
@brief the bytes [start, end) of node @a n in the source, as materialize() sets them
Scalars as position_source::set_scalar; an array or object from its opening
bracket to its closing bracket, which is found by following the chain of last
elements to a scalar (or an empty container) and scanning from there to the
closing brackets. None (npos) for values written by edits, and for containers
whose last element has none, as with materialize().
*/
template<bool Editable>
void value_range(const document_data& d, const node* n, std::size_t& start, std::size_t& end)
{
using nav = navigation<Editable>;
position_source p;
start = std::string::npos;
end = std::string::npos;
if (!is_container(*n))
{
p.set_scalar(d, *n);
start = p.start;
end = p.end;
return;
}
// the open brackets: n, then the last element as long as it is a non-empty container
std::size_t closing = 0;
std::size_t pos = std::string::npos;
for (const node* c = n;;)
{
if (!is_container(*c))
{
p.set_scalar(d, *c);
pos = p.end;
break;
}
if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size)
{
return;
}
++closing;
const node* const first = nav::first(d, c);
const node* const last_end = nav::end(d, c);
if (first == last_end)
{
pos = std::size_t{c->off} + 1;
break;
}
const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0;
const node* last = first;
for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset))
{
last = e;
}
c = nav::value(last + value_offset);
}
for (; closing != 0 && pos != std::string::npos; --closing)
{
pos = position_source::container_end(d.base[0], d.size, pos);
}
if (pos != std::string::npos)
{
start = n->off;
end = pos;
}
}
#endif
/// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only
/// if there is a path)
///
/// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be
/// thrown: instantiated in every translation unit (as a non-template inline
/// function makes it), it changed GCC's early inlining of unrelated view
/// functions, so that GCC 13 no longer inlined a recursive walk over a view
/// into itself (2.7 times the instructions).
#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS
template<typename Unused = void>
std::string diagnostics(const error_site site)
{
if (site.doc() == nullptr || site.value() == nullptr)
{
return "";
}
const document_data& d = *site.doc();
const bool edited = d.edits != nullptr;
std::string context;
#if JSON_DIAGNOSTICS
std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found || tokens.empty())
{
// context is still empty; returning it keeps the copy elided (-Wnrvo)
return context;
}
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
#endif
#if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0;
std::size_t end = 0;
if (edited)
{
value_range<true>(d, site.value(), start, end);
}
else
{
value_range<false>(d, site.value(), start, end);
}
if (start != std::string::npos && end != std::string::npos)
{
context += concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") ");
}
#endif
return context;
}
#else
template<typename Unused = void>
std::string diagnostics(const error_site /*site*/)
{
return "";
}
#endif
/// the context of an exception about view @a v (see exception_prefix())
template<typename View>
std::string diagnostics_of(const View& v)
{
return diagnostics<>(site_of(v));
}
// Exceptions are thrown out of line, so that the accessors that may throw stay // Exceptions are thrown out of line, so that the accessors that may throw stay
// small enough to be inlined. // small enough to be inlined. The overloads with an error_site put its context
// in front of the message.
//
// The overloads without one are for exceptions that are not about a value of a
// document: limits and invalid input of the editing and image functions, a
// discarded view (it has no node), a view of another document (its path would
// be one in that document), and those that basic_json creates without a
// context too (the errors of json_pointer, invalid UTF-8). The one exception
// about a value that has no site is key(), see view_iterator::key().
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type)
{ {
@@ -45,6 +330,24 @@ namespace view
NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr));
} }
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_type_error(const error_site site, exception_id id, const char* prefix, const char* type)
{
NLOHMANN_VIEW_THROW(type_error::create(id, concat(diagnostics<>(site), prefix, type), nullptr));
}
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_out_of_range(const error_site site, exception_id id, const std::string& msg)
{
NLOHMANN_VIEW_THROW(out_of_range::create(id, concat(diagnostics<>(site), msg), nullptr));
}
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_invalid_iterator(const error_site site, exception_id id, const char* msg)
{
NLOHMANN_VIEW_THROW(invalid_iterator::create(id, concat(diagnostics<>(site), msg), nullptr));
}
/// a parse error without a position (as those of json_pointer) /// a parse error without a position (as those of json_pointer)
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(exception_id id, const std::string& msg) [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(exception_id id, const std::string& msg)
{ {
@@ -53,22 +356,23 @@ namespace view
/// out_of_range.401: the array index @a idx is not below the size /// out_of_range.401: the array index @a idx is not below the size
template<typename Index> template<typename Index>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const Index idx) [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const error_site site, const Index idx)
{ {
throw_out_of_range(exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range")); throw_out_of_range(site, exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range"));
} }
/// out_of_range.402: the JSON Pointer token '-' of an array of @a size elements /// out_of_range.402: the JSON Pointer token '-' of an array of @a size elements
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(std::size_t size) template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_past_end(const error_site site, std::size_t size)
{ {
throw_out_of_range(exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range")); throw_out_of_range(site, exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range"));
} }
/// out_of_range.403: the object has no member @a key /// out_of_range.403: the object has no member @a key
template<typename Key> template<typename Key>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key) [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const error_site site, const Key& key)
{ {
throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found")); throw_out_of_range(site, exception_id::key_not_found, concat("key '", key, "' not found"));
} }
/// SAX handler of the error replay: accepts every event without storing it, /// SAX handler of the error replay: accepts every event without storing it,
@@ -112,10 +416,12 @@ struct throwing_sax
return true; return true;
} }
// LCOV_EXCL_START (throw_parse_failure parses JSON only)
bool binary(binary_t& /*unused*/) bool binary(binary_t& /*unused*/)
{ {
return true; return true;
} }
// LCOV_EXCL_STOP
bool start_object(std::size_t /*unused*/) bool start_object(std::size_t /*unused*/)
{ {
-4
View File
@@ -787,10 +787,6 @@ inline void load_image(document_data& d, const std::uint8_t* image, std::size_t
d.edits.reset(); d.edits.reset();
d.base[2] = nullptr; d.base[2] = nullptr;
d.owned.clear(); d.owned.clear();
if (d.owned_image.empty() || image != d.owned_image.data())
{
d.owned_image.clear();
}
d.arena.clear(); d.arena.clear();
d.indexes.clear(); d.indexes.clear();
d.index_slots.clear(); d.index_slots.clear();
+4 -1
View File
@@ -105,7 +105,10 @@ class view_iterator
} }
/// the key of the current object member; throws invalid_iterator.207 for /// the key of the current object member; throws invalid_iterator.207 for
/// other iterators, like basic_json's iterators /// other iterators, like basic_json's iterators (but without the context
/// JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS add: the iterator does not
/// know the value it iterates over, and keeping it would make iterators
/// larger, which keeps GCC from inlining recursive walks)
string_view_t key() const string_view_t key() const
{ {
if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0))
@@ -40,6 +40,15 @@
#define NLOHMANN_VIEW_NOINLINE #define NLOHMANN_VIEW_NOINLINE
#endif #endif
// GCC: a function compiled as written, without the clones in which interprocedural
// constant propagation drops an argument that every caller passes as the same constant
// (calling such a clone changes the code GCC generates for the parse loop that calls it)
#if defined(__GNUC__) && !defined(__clang__)
#define NLOHMANN_VIEW_NOCLONE __attribute__((noclone))
#else
#define NLOHMANN_VIEW_NOCLONE
#endif
#if defined(__GNUC__) || defined(__clang__) #if defined(__GNUC__) || defined(__clang__)
#define NLOHMANN_VIEW_NODISCARD __attribute__((warn_unused_result)) #define NLOHMANN_VIEW_NODISCARD __attribute__((warn_unused_result))
#elif defined(_MSC_VER) #elif defined(_MSC_VER)
@@ -15,6 +15,7 @@
#undef NLOHMANN_VIEW_UNLIKELY #undef NLOHMANN_VIEW_UNLIKELY
#undef NLOHMANN_VIEW_ALWAYS_INLINE #undef NLOHMANN_VIEW_ALWAYS_INLINE
#undef NLOHMANN_VIEW_NOINLINE #undef NLOHMANN_VIEW_NOINLINE
#undef NLOHMANN_VIEW_NOCLONE
#undef NLOHMANN_VIEW_NODISCARD #undef NLOHMANN_VIEW_NODISCARD
#undef NLOHMANN_VIEW_THROW #undef NLOHMANN_VIEW_THROW
#undef NLOHMANN_VIEW_LITTLE_ENDIAN #undef NLOHMANN_VIEW_LITTLE_ENDIAN
+3 -8
View File
@@ -65,14 +65,9 @@ struct position_source
return token{end - start}; return token{end - start};
} }
/// the range [first, last), or none if either is npos /// the range [first, last)
void set(std::size_t first, std::size_t last) noexcept void set(std::size_t first, std::size_t last) noexcept
{ {
if (first == std::string::npos || last == std::string::npos)
{
clear();
return;
}
start = first; start = first;
end = last; end = last;
} }
@@ -191,11 +186,11 @@ struct position_source
first = n.off; first = n.off;
last = n.off + 4u; last = n.off + 4u;
break; break;
case node_kind::object: case node_kind::object: // LCOV_EXCL_LINE
case node_kind::array: case node_kind::array:
case node_kind::link: case node_kind::link:
default: default:
break; break; // LCOV_EXCL_LINE
} }
// (a crafted image can point past the text) // (a crafted image can point past the text)
if (last == std::string::npos || last > d.size) if (last == std::string::npos || last > d.size)
+163 -8
View File
@@ -19,6 +19,7 @@
#include <nlohmann/detail/view/macro_scope.hpp> #include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/node.hpp> #include <nlohmann/detail/view/node.hpp>
#include <nlohmann/detail/view/scan.hpp> #include <nlohmann/detail/view/scan.hpp>
#include <nlohmann/detail/view/simd.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail namespace detail
@@ -103,12 +104,56 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n)
return convert_float<FloatType>(first, last, dot, mantissa_end); return convert_float<FloatType>(first, last, dot, mantissa_end);
} }
#if NLOHMANN_VIEW_VECTOR
/*!
@brief the value of the first 16 digits of a run of digits with a point after
int_digits of them (or none, if int_digits >= 16)
The 17 bytes at p are read at once: the bytes after the point move down by one,
then pairs, quadruples, and octets of digits are combined by multiplications
(after simdjson). The bytes [p, p + 32) must be readable. Bytes that are not
digits (only in a damaged image loaded with image_check::bounds) give a wrong
value, but no undefined behavior.
*/
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t sixteen_digits(const unsigned char* p, unsigned int_digits) noexcept
{
#if NLOHMANN_VIEW_NEON
const uint8x16_t low = vld1q_u8(p);
const uint8x16_t index = vcombine_u8(vcreate_u8(0x0706050403020100u), vcreate_u8(0x0F0E0D0C0B0A0908u));
const uint8x16_t before_point = vcltq_u8(index, vdupq_n_u8(static_cast<std::uint8_t>(int_digits)));
const uint8x16_t digits = vsubq_u8(vbslq_u8(before_point, low, vextq_u8(low, vld1q_u8(p + 16), 1)), vdupq_n_u8('0'));
const uint16x8_t twos = vpaddlq_u8(vmulq_u8(digits, vreinterpretq_u8_u16(vdupq_n_u16(0x010A))));
const uint32x4_t fours = vpaddlq_u16(vmulq_u16(twos, vreinterpretq_u16_u32(vdupq_n_u32(0x00010064))));
const uint64x2_t eights = vpaddlq_u32(vmulq_u32(fours, vreinterpretq_u32_u64(vdupq_n_u64(0x0000000100002710u))));
return (vgetq_lane_u64(eights, 0) * 100000000u) + vgetq_lane_u64(eights, 1);
#else
// NOLINTBEGIN(portability-simd-intrinsics)
const __m128i low = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
const __m128i high = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p + 16))); // NOLINT(bugprone-casting-through-void)
const __m128i before_point = _mm_cmpgt_epi8(_mm_set1_epi8(static_cast<char>(int_digits)), _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15));
const __m128i moved = _mm_or_si128(_mm_srli_si128(low, 1), _mm_slli_si128(high, 15));
const __m128i digits = _mm_sub_epi8(_mm_or_si128(_mm_and_si128(before_point, low), _mm_andnot_si128(before_point, moved)), _mm_set1_epi8('0'));
// (16-bit lanes: SSE2 has no multiply-add of bytes)
const __m128i tens = _mm_setr_epi16(10, 1, 10, 1, 10, 1, 10, 1);
const __m128i twos = _mm_packs_epi32(_mm_madd_epi16(_mm_unpacklo_epi8(digits, _mm_setzero_si128()), tens),
_mm_madd_epi16(_mm_unpackhi_epi8(digits, _mm_setzero_si128()), tens));
const __m128i fours = _mm_madd_epi16(twos, _mm_setr_epi16(100, 1, 100, 1, 100, 1, 100, 1));
const __m128i eights = _mm_madd_epi16(_mm_packs_epi32(fours, fours), _mm_setr_epi16(10000, 1, 10000, 1, 10000, 1, 10000, 1));
const auto upper = static_cast<std::uint32_t>(_mm_cvtsi128_si32(eights));
const auto lower = static_cast<std::uint32_t>(_mm_cvtsi128_si32(_mm_srli_si128(eights, 4)));
// NOLINTEND(portability-simd-intrinsics)
return (std::uint64_t{upper} * 100000000u) + lower;
#endif
}
#endif
/*! /*!
@brief the digits of a float token with at most 19 digits, from its layout @brief the digits of a float token with at most 19 digits, from its layout
The digit layout recorded while parsing says where the integer digits, the The digit layout recorded while parsing says where the integer digits, the
fraction digits, and the exponent are, so the digits are read eight at a fraction digits, and the exponent are, so the digits are read eight at a
time without scanning. time without scanning (with SIMD, a token of 16 to 19 digits sixteen at a
time).
@param[in] p first character of the token @param[in] p first character of the token
@param[in] e end of the token @param[in] e end of the token
@@ -118,15 +163,34 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
{ {
const bool negative = *p == '-'; const bool negative = *p == '-';
p += negative ? 1 : 0; p += negative ? 1 : 0;
std::uint64_t w = parse_upto19(p, int_digits, limit); std::uint64_t w = 0;
p += int_digits;
std::int64_t q = 0; std::int64_t q = 0;
if (frac_digits != 0) #if NLOHMANN_VIEW_VECTOR
const unsigned count = int_digits + frac_digits;
if (count >= 16 && limit - p >= 32)
{ {
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit); // (the point, if any, follows the integer digits; the digits after
p += 1 + frac_digits; // the first 16 are at most three)
w = sixteen_digits(p, int_digits);
for (unsigned i = 16; i < count; ++i)
{
w = (w * 10) + static_cast<std::uint64_t>(p[i + (i >= int_digits ? 1 : 0)] - '0');
}
p += count + (frac_digits != 0 ? 1 : 0);
q = -static_cast<std::int64_t>(frac_digits); q = -static_cast<std::int64_t>(frac_digits);
} }
else /* NOLINT(readability/braces) */
#endif
{
w = parse_upto19(p, int_digits, limit);
p += int_digits;
if (frac_digits != 0)
{
w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
p += 1 + frac_digits;
q = -static_cast<std::int64_t>(frac_digits);
}
}
if (p != e) if (p != e)
{ {
// [eE][+-]digits; huge exponents saturate (the parser rejected // [eE][+-]digits; huge exponents saturate (the parser rejected
@@ -161,12 +225,22 @@ NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char
The result is correctly rounded by the lexer's conversion The result is correctly rounded by the lexer's conversion
(detail::decimal_to_float(): Clinger's fast path where both operands are (detail::decimal_to_float(): Clinger's fast path where both operands are
exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19 exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19
digits), so it is the value parse() produces. digits), so it is the value parse() produces. Tokens of 16 or more digits go
to Eisel-Lemire directly: the fast path takes few of them (none of 17 digits),
and its check is a branch that data like GeoJSON coordinates mispredicts.
*/ */
template<typename FloatType> template<typename FloatType>
NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept
{ {
return decimal_to_float<FloatType>(layout_decimal(p, e, int_digits, frac_digits, limit)); const float_significand d = layout_decimal(p, e, int_digits, frac_digits, limit);
if (int_digits + frac_digits >= 16)
{
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
native_float_t<FloatType> value{};
float_from_bits(eisel_lemire<format>(d.exponent, d.w) | (d.negative ? (std::uint64_t{1} << format::sign_bit()) : 0u), value);
return value;
}
return decimal_to_float<FloatType>(d);
} }
/// the value of a float set by an edit: its token (the shortest round-trip /// the value of a float set by an edit: its token (the shortest round-trip
@@ -185,6 +259,80 @@ NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n)
return float_value<FloatType>(token, n); return float_value<FloatType>(token, n);
} }
#if NLOHMANN_VIEW_VECTOR
/// eisel_lemire() for the token w * 10^q of coordinate_float()
template<typename Format>
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, Format /*format*/) noexcept
{
return eisel_lemire<Format>(q, w);
}
/*!
@brief (binary64) eisel_lemire() without the cases that tokens of 16 or 17
digits with 5 or more after the point cannot reach: a value of 0, a
subnormal, an infinity, or a tie (q < -4)
*/
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t coordinate_bits(std::int64_t q, std::uint64_t w, ieee_binary_format<53> format) noexcept
{
if (NLOHMANN_VIEW_UNLIKELY(w == 0 || q > -5))
{
return eisel_lemire<ieee_binary_format<53>>(q, w);
}
static_cast<void>(format);
const int lz = count_leading_zeros(w);
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
std::uint64_t high = product.high;
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
{
// (the lower bits may carry into the result)
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
high += product.low + second < product.low ? 1u : 0u;
}
const auto upperbit = static_cast<unsigned>(high >> 63u);
std::uint64_t mantissa = high >> (upperbit + 9);
auto power2 = static_cast<std::uint64_t>(((217706 * q) >> 16) + 63 + static_cast<std::int64_t>(upperbit) - lz + 1023);
mantissa = (mantissa + (mantissa & 1u)) >> 1u;
const bool carry = mantissa >= (std::uint64_t{2} << 52u);
mantissa = carry ? (std::uint64_t{1} << 52u) : mantissa;
power2 += carry ? 1u : 0u;
return (mantissa & ~(std::uint64_t{1} << 52u)) | (power2 << 52u);
}
/*!
@brief (binary32 and binary64) the value of a float token of the source with
16 or 17 digits, a point, and no exponent, the shape of most coordinates
written with full precision; whether the token of n has this shape
These tokens skip the general layout of layout_decimal(): no digits beyond
the 17th, no exponent (and Eisel-Lemire directly, as in layout_float()).
*/
template<typename FloatType>
NLOHMANN_VIEW_ALWAYS_INLINE bool coordinate_float(const document_data& d, const node& n, FloatType& value) noexcept
{
const unsigned int_digits = n.extra & 0xFFu;
const unsigned frac_digits = n.extra >> 8u;
const unsigned count = int_digits + frac_digits;
const auto* const first = reinterpret_cast<const unsigned char*>(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const unsigned negative = first[0] == '-' ? 1u : 0u;
if (count - 16u > 1u || frac_digits == 0 || n.len != negative + count + 1 || d.size - n.off < 33)
{
return false;
}
const unsigned char* const digits = first + negative;
const std::uint64_t w = sixteen_digits(digits, int_digits);
// (the 17th digit follows the point)
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(digits[17] - '0');
using format = ieee_binary_format<std::numeric_limits<FloatType>::digits>;
native_float_t<FloatType> result{};
float_from_bits(coordinate_bits(-static_cast<std::int64_t>(frac_digits), count == 17 ? w17 : w, format{}) | (static_cast<std::uint64_t>(negative) << format::sign_bit()), result);
value = result;
return true;
}
#endif
/// the value of the float token of a node, as parse() converts it; floats and /// the value of the float token of a node, as parse() converts it; floats and
/// doubles with at most 19 digits are converted from the digit layout /// doubles with at most 19 digits are converted from the digit layout
template<typename FloatType> template<typename FloatType>
@@ -200,6 +348,13 @@ FloatType float_value(const document_data& d, const node& n)
template<typename FloatType> template<typename FloatType>
FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/) FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/)
{ {
#if NLOHMANN_VIEW_VECTOR
FloatType value {};
if (coordinate_float(d, n, value))
{
return value;
}
#endif
const unsigned int_digits = n.extra & 0xFFu; const unsigned int_digits = n.extra & 0xFFu;
const unsigned frac_digits = n.extra >> 8u; const unsigned frac_digits = n.extra >> 8u;
if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many") if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many")
+5 -4
View File
@@ -44,6 +44,7 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept
} }
/// throws the exception json_pointer::array_index throws for this status /// throws the exception json_pointer::array_index throws for this status
/// (without a context, as json_pointer::array_index)
template<typename StringType> template<typename StringType>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s)
{ {
@@ -83,7 +84,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{ {
if (mode == pointer_mode::checked) if (mode == pointer_mode::checked)
{ {
throw_key_not_found(token); throw_key_not_found(site_of(cur), token);
} }
return View(); return View();
} }
@@ -95,7 +96,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{ {
if (throwing) if (throwing)
{ {
throw_array_index_past_end(cur.size()); throw_array_index_past_end(site_of(cur), cur.size());
} }
return View(); return View();
} }
@@ -114,7 +115,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{ {
if (mode == pointer_mode::checked) if (mode == pointer_mode::checked)
{ {
throw_array_index_out_of_range(idx); throw_array_index_out_of_range(site_of(cur), idx);
} }
return View(); return View();
} }
@@ -124,7 +125,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{ {
if (throwing) if (throwing)
{ {
throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); throw_out_of_range(site_of(cur), exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'"));
} }
return View(); return View();
} }
+51
View File
@@ -198,6 +198,57 @@ inline const unsigned char* find_block_comment_end(const unsigned char* p, const
return p; return p;
} }
/// The digits of a fraction, whose number varies from one number to the next: skip_digits() with 16 bytes compared at
/// once and the end of the run taken from a bit mask (SSE2), instead of a compare and a branch for each digit. That
/// is about a third of the instructions for a fraction of 12 digits, and no branch that is mispredicted when the
/// lengths vary. Where there is no SSE2, skip_digits() is used.
#if NLOHMANN_VIEW_SSE2
// The last bytes of the input, one at a time. GCC unrolls this loop in every inlined copy of the function below, which
// makes the parse loop 4 KiB larger and slows down documents that have hardly any fractions; out of line, it does
// not. Clang does better with the loop inline (a call in the parse loop costs it more than the unrolling).
#if defined(__GNUC__) && !defined(__clang__)
inline NLOHMANN_VIEW_NOINLINE const unsigned char* skip_digits_tail(const unsigned char* p, const unsigned char* e) noexcept
{
while (p != e && is_digit(*p))
{
++p;
}
return p;
}
#else
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits_tail(const unsigned char* p, const unsigned char* e) noexcept
{
while (p != e && is_digit(*p))
{
++p;
}
return p;
}
#endif
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_fraction_digits(const unsigned char* p, const unsigned char* e) noexcept
{
while (e - p >= 16)
{
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
// a digit is above '/' and below ':' (signed compares: a byte with the top bit set is negative, and neither)
const __m128i digits = _mm_and_si128(_mm_cmpgt_epi8(in, _mm_set1_epi8('/')), _mm_cmplt_epi8(in, _mm_set1_epi8(':')));
const auto not_digit = static_cast<std::uint64_t>(~static_cast<unsigned>(_mm_movemask_epi8(digits)) & 0xFFFFu);
if (not_digit != 0)
{
return p + count_trailing_zeros(not_digit);
}
p += 16;
}
return skip_digits_tail(p, e);
}
#else
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_fraction_digits(const unsigned char* p, const unsigned char* e) noexcept
{
return skip_digits(p, e);
}
#endif
/// powers of ten up to 10^19 as integers /// powers of ten up to 10^19 as integers
inline std::uint64_t int_pow10(unsigned k) noexcept inline std::uint64_t int_pow10(unsigned k) noexcept
{ {
+410 -30
View File
@@ -14,7 +14,9 @@
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t #include <cstdint> // uint8_t, uint32_t
#include <cstring> // memcpy, memset #include <cstring> // memcpy, memset
#include <functional> // less
#include <limits> // numeric_limits #include <limits> // numeric_limits
#include <memory> // unique_ptr
#include <type_traits> // integral_constant #include <type_traits> // integral_constant
#include <vector> // vector #include <vector> // vector
@@ -384,6 +386,143 @@ struct depth_counter<false>
} }
}; };
/*!
@brief where the double x nearest to a token w * 10^q lies, for 10^15 <= w <
10^17 and -17 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
dump() writes the shortest decimal in the rounding interval of x (the closest
one if there are several, see zmij::to_shortest()). Its digits are those of a
multiple of 10^g of the token's last digit, with g = dec_exp + 1 - q for x's
decimal exponent dec_exp: the multiple of 10^g nearest to x if it lies in the
interval (at most one does), else the multiple of 10^(g - 1) nearest to x
(see token_digits_1() and token_digits_2()). For g <= 0, the token itself is
in the interval.
The position is exact in a 64-bit fixed-point number with 56 bits after the
point (of the token's last digit): x / 10^q = 2c * 5^-q / 2^t for x = c * 2^e2
and t = q + 1 - e2, with at most 56 bits after the point here, and |x / 10^q -
w| is at most half the spacing of the doubles, below 12 units. So only the low
64 bits of 2c * 5^-q are needed. c and e2 come from Eisel-Lemire's product
directly (as eisel_lemire() rounds it: no tie is possible for q < -4, and its
second product is added where it can carry into the result).
@param[out] offset x / 10^q - w + 32, with 56 bits after the point
@param[out] radius half the spacing of the doubles at x, likewise (+ 1 if
the interval is closed)
@return g; 3 if the digits are not decided here (a power of two, whose
interval is not symmetric, or out of range)
*/
NLOHMANN_VIEW_ALWAYS_INLINE int locate_token(std::uint64_t w, int q, std::uint64_t& offset, std::uint64_t& radius) noexcept
{
const int lz = count_leading_zeros(w);
const std::uint64_t normalized = w << static_cast<unsigned>(lz);
const auto index = static_cast<std::size_t>(2 * (q - pow5_128_smallest_power));
const uint128_parts product = full_multiplication(normalized, pow5_128()[index]);
std::uint64_t high = product.high;
if (NLOHMANN_VIEW_UNLIKELY((high & 0x1FFu) == 0x1FFu))
{
// (as eisel_lemire(): the lower bits may carry into the result)
const std::uint64_t second = full_multiplication(normalized, pow5_128()[index + 1]).high;
high += product.low + second < product.low ? 1u : 0u;
}
const auto upperbit = static_cast<unsigned>(high >> 63u);
const std::uint64_t mantissa = high >> (upperbit + 9);
const std::uint64_t c = (mantissa + (mantissa & 1u)) >> 1u;
// e2 as eisel_lemire() computes the exponent (log2(10) ~ 217706 / 2^16)
const int e2 = static_cast<int>((217706 * static_cast<std::int64_t>(q)) >> 16) + 63 + static_cast<int>(upperbit) - lz - 52;
const int t = q + 1 - e2;
const int g = ((e2 * 315653) >> 20) + 1 - q; // (dec_exp as in zmij::to_shortest(), for a regular x)
static const std::array<std::uint64_t, 17> fives = {{1u, 5u, 25u, 125u, 625u, 3125u, 15625u, 78125u, 390625u, 1953125u, 9765625u, 48828125u, 244140625u, 1220703125u, 6103515625u, 30517578125u, 152587890625u}};
const std::uint64_t five = fives[static_cast<std::size_t>(-q)]; // 5^-q
const unsigned s = static_cast<unsigned>(56 - t) & 63u;
offset = ((2 * c * five) << s) - (w << 56u) + (std::uint64_t{32} << 56u);
radius = (five << s) + ((c & 1u) == 0 ? 1u : 0u);
// (c = 2^52: a power of two; c = 2^53: rounded up to one. With 8 to 16
// fraction digits, as the batch's tokens have, t is in [15, 40] and g at
// most 2. x lies halfway between two candidates only if its distance from
// the token is a whole unit (g == 2) or a half (g == 1): such tokens are
// left to the full conversion)
const bool decided = c - ((std::uint64_t{1} << 52u) + 1) < (std::uint64_t{1} << 52u) - 1
&& (offset & ((std::uint64_t{1} << 55u) - 1)) != 0;
return decided ? g : 3;
}
/// all bits set if b, else none
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t mask_if(bool b) noexcept
{
return std::uint64_t{0} - static_cast<std::uint64_t>(b);
}
/// a if b, else c, without a branch (the conditions of token_digits_1/2() are
/// as good as random, and GCC compiled their ?: as branches: one misprediction
/// per float of canada.json on x86-64)
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t select_if(bool b, std::uint64_t a, std::uint64_t c) noexcept
{
return c ^ ((a ^ c) & mask_if(b));
}
/// the distance of a position (see locate_token()) from the biased point 32
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t biased_distance(std::uint64_t position) noexcept
{
constexpr std::uint64_t center = std::uint64_t{32} << 56u;
// |position - center|, without a branch: negated (complemented, plus 1) if below
const std::uint64_t below = mask_if(position < center);
return ((position - center) ^ below) - below;
}
/*!
@brief (g == 1) the digits dump() writes for a token, from the position of its
double (see locate_token()): the multiple of 10 nearest to x if it is in the
interval, else w rounded to x (an exact tie between two candidates, like
754570942.33984373, whose double is 754570942.33984375, is excluded by
locate_token())
With |x - w| at most the radius, which is below half the spacing of the
candidates, the multiple of 10 nearest to x is w - r10 or the one above it.
*/
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_1(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10) noexcept
{
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
const std::uint64_t from = offset + (r10 * one); // x - (w - r10) (+ 32)
const bool up = from > 37 * one; // (nearer to the multiple above)
const bool shorter = biased_distance(from - (mask_if(up) & (10 * one))) < radius;
const std::uint64_t a = w - r10 + (mask_if(up) & 10u);
const std::uint64_t b = w + ((offset + (one / 2)) >> 56u) - 32; // w rounded to x
return select_if(shorter, a, b);
}
/*!
@brief (g == 2) the digits dump() writes for a token, as token_digits_1(): the
multiple of 100 nearest to x if it is in the interval, else the multiple of 10
nearest to x
*/
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t token_digits_2(std::uint64_t w, std::uint64_t offset, std::uint64_t radius, std::uint64_t r10, std::uint64_t r100) noexcept
{
constexpr std::uint64_t one = std::uint64_t{1} << 56u;
const std::uint64_t from = offset + (r100 * one);
const bool up = from > 82 * one;
const bool shorter = biased_distance(from - (mask_if(up) & (100 * one))) < radius;
const std::uint64_t a = w - r100 + (mask_if(up) & 100u);
// x - (w - r10), in [-12, 21]: the multiple of 10 nearest to x is that one,
// or one or two above, or one below
const std::uint64_t y = offset + (r10 * one);
const std::uint64_t steps = static_cast<std::uint64_t>(y > 37 * one) + static_cast<std::uint64_t>(y > 47 * one);
const bool down = y < 27 * one;
const std::uint64_t b = w - r10 + (10 * steps) - (mask_if(down) & 10u);
return select_if(shorter, a, b);
}
/// the four ASCII digits of x < 10^4, the first in the lowest byte (as
/// little-endian memory holds them)
NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t four_digit_text(std::uint32_t x) noexcept
{
const std::uint32_t ab = (x * 5243u) >> 19u; // x / 100
std::uint32_t v = ab | ((x - (ab * 100u)) << 16u);
const std::uint32_t a = ((v * 103u) >> 10u) & 0x000F000Fu; // / 10 per 16-bit lane
v = a | ((v - (a * 10u)) << 8u);
return v + 0x30303030u;
}
/// how the view's dump() writes a value /// how the view's dump() writes a value
struct dump_style struct dump_style
{ {
@@ -502,7 +641,7 @@ class view_serializer
{ {
invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8) invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8)
} }
write_codepoint<false>(s[i], s + i, 1); // a quote, a backslash, or a control character write_escaped(s[i]); // a quote, a backslash, or a control character
++i; ++i;
} }
m_out.put('"'); m_out.put('"');
@@ -711,6 +850,7 @@ class view_serializer
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::vector<frame> heap; std::vector<frame> heap;
small_stack<frame> stack(buffer.data(), buffer.size(), heap); small_stack<frame> stack(buffer.data(), buffer.size(), heap);
float_batch batch;
const char* const src = m_doc.src; const char* const src = m_doc.src;
const char* const src_end = src + m_doc.size; const char* const src_end = src + m_doc.size;
char* w = m_out.cursor(); char* w = m_out.cursor();
@@ -850,7 +990,7 @@ class view_serializer
else if (enabled(fast_double::value)) else if (enabled(fast_double::value))
{ {
room(64); room(64);
w = write_double_at(w, *n); w = write_double_ahead(w, src, n, batch);
} }
else else
{ {
@@ -875,6 +1015,20 @@ class view_serializer
case value_t::array: case value_t::array:
{ {
const bool object = n->kind == node_kind::object; const bool object = n->kind == node_kind::object;
// (read-only documents, compact output) a pair of floats,
// like the coordinates of GeoJSON, without a frame
if (enabled(!Editable && !Pretty && !SourceNumbers && fast_double::value) && n->next == 3 && n->len == 2
&& n[1].kind == node_kind::number_float && n[2].kind == node_kind::number_float)
{
room(2 * 64);
*w++ = '[';
w = write_double_ahead(w, src, n + 1, batch);
*w++ = ',';
w = write_double_ahead(w, src, n + 2, batch);
*w++ = ']';
n += 2; // (the element before the next node)
break;
}
room(8); room(8);
if (n->len == 0) if (n->len == 0)
{ {
@@ -984,23 +1138,261 @@ class view_serializer
} }
} }
/// (float_batch) the mark of digits whose rounding carried beyond the
/// token's last four (see mark())
static constexpr std::uint64_t carried() noexcept
{
return std::uint64_t{1} << 63u;
}
/// floats of 16 or 17 digits ahead of the walk, converted together (see
/// convert_ahead()), with their digits as mark() keeps them
struct float_batch
{
static constexpr std::size_t capacity() noexcept
{
return 256;
}
/// (on the heap, allocated by the first conversion: a frame of more
/// than a page made every dump of a small document slower)
struct entries
{
std::array<const node*, 256> at; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
};
std::unique_ptr<entries> storage{};
const node** at = nullptr;
std::uint64_t* digits = nullptr;
std::size_t count = 0;
std::size_t next = 0;
const node* scanned = nullptr; ///< where the last scan of the node array ended
};
/// (doubles) the float at n as dump() writes it, at w (64 bytes of room):
/// from the batch if it holds n
NLOHMANN_VIEW_ALWAYS_INLINE char* write_double_ahead(char* w, const char* src, const node* n, float_batch& batch)
{
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
// (digits 0: not written by the batch; carried(): rounding carried
// beyond the last four digits)
if (batch.next != batch.count && batch.at[batch.next] == n && batch.digits[batch.next] - 1 < carried() - 1)
{
return write_token_digits(w, src, *n, batch.digits[batch.next++]);
}
return write_double_beside(w, n, batch);
#else
static_cast<void>(src);
static_cast<void>(batch);
return write_double_at(w, *n);
#endif
}
#if NLOHMANN_VIEW_VECTOR && NLOHMANN_VIEW_LITTLE_ENDIAN
/// write_double_ahead() for a float that is not the batch's next one (out
/// of line: the walk is faster with less code in its loop)
NLOHMANN_VIEW_NOINLINE char* write_double_beside(char* w, const node* n, float_batch& batch)
{
// (an editable document's walk can skip nodes of the array)
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
while (batch.next != batch.count && before(batch.at[batch.next], n))
{
++batch.next;
}
if ((batch.next != batch.count && batch.at[batch.next] == n) || convert_ahead(n, batch))
{
const std::uint64_t digits = batch.digits[batch.next++];
if ((digits & carried()) != 0)
{
return write_carried_digits(w, *n, digits & ~carried());
}
if (digits != 0)
{
return write_token_digits(w, m_doc.src, *n, digits);
}
}
return write_double_at(w, *n);
}
/*!
@brief convert the floats of 16 or 17 digits among the next nodes of the
array at once; whether n is the first of them
Such tokens (like the coordinates of GeoJSON, written with 17 digits) are
written as the token with its last four digits replaced
(write_token_digits()). Each step (the digits, locate_token(),
token_digits_1/2()) is a long chain of dependent operations; one loop per
step over many floats lets the processor overlap the floats, which it does
not across the steps of one float (on Apple M1, one loop for the digits and
locate_token() made the dump of canada.json 12 % slower). Each node of the array
is scanned once; a node that the walk does not reach in order (in an
editable document) is written without the batch.
*/
NLOHMANN_VIEW_NOINLINE bool convert_ahead(const node* n, float_batch& batch)
{
const std::less<const node*> before{}; // NOLINT(modernize-use-transparent-functors) C++11 has no std::less<>
const node* const last = m_doc.nodes + m_doc.node_count;
if (before(n, batch.scanned) || before(n, m_doc.nodes) || !before(n, last))
{
return false;
}
const char* const src = m_doc.src;
const char* const src_end = src + m_doc.size;
const node* const stop = last - n > 1024 ? n + 1024 : last;
batch.count = 0;
batch.next = 0;
// the digits of each token (and its last four from the text)
std::array<std::uint64_t, 256> digits; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<int, 256> exponent; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<std::uint32_t, 256> last_four; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
const node* p = n;
if (!batch.storage)
{
batch.storage.reset(new typename float_batch::entries); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): C++11
batch.at = batch.storage->at.data();
batch.digits = batch.storage->digits.data();
}
for (; p != stop && batch.count != float_batch::capacity(); ++p)
{
// a token of the source: 16 or 17 digits, at least 8 after the
// point, no exponent, 32 bytes to read
const unsigned frac_digits = p->extra >> 8u;
const unsigned int_digits = p->extra & 0xFFu;
const unsigned count = int_digits + frac_digits;
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
{
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const unsigned sign = token[0] == '-' ? 1 : 0;
const unsigned char* const first = token + sign;
const std::uint64_t w = sixteen_digits(first, int_digits);
// (the 17th digit follows the point; a token with an exponent
// gets digits outside the range of locate_token(), and is not
// written by the batch. A token 0.xxx with 16 significant
// digits is: its double is written as "0." and its digits too)
const std::uint64_t w17 = (w * 10) + static_cast<std::uint64_t>(first[17] - '0');
const std::size_t i = batch.count++;
batch.at[i] = p;
if (p->len != sign + count + 1)
{
digits[i] = 0;
}
else
{
digits[i] = count == 17 ? w17 : w;
}
exponent[i] = -static_cast<int>(frac_digits);
// (byte 0: the last digit, byte 1: the last two, bits 16-29: the last four)
std::uint32_t four = 0;
std::memcpy(&four, token + p->len - 4, 4);
four -= 0x30303030u;
const std::uint32_t pairs = ((four & 0x0F0F0F0Fu) * 2561u) >> 8u; // (byte 0, 2: the pairs)
last_four[i] = (four >> 24u) | ((pairs >> 16u & 0xFFu) << 8u) | ((((pairs & 0x00FF00FFu) * 6553601u) >> 16u) << 16u);
}
}
batch.scanned = p;
// the position of each double, and the floats of either kind
std::array<std::uint64_t, 256> offset; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<std::uint64_t, 256> radius; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<std::uint8_t, 256> kind_1; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::array<std::uint8_t, 256> kind_2; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
std::size_t count_1 = 0;
std::size_t count_2 = 0;
for (std::size_t i = 0; i != batch.count; ++i)
{
const std::uint64_t w = digits[i];
// (the range excludes the values of bytes that are not digits, only
// in a damaged image loaded with image_check::bounds)
const int g = w - 1000000000000000u < 99000000000000000u ? locate_token(w, exponent[i], offset[i], radius[i]) : 3;
batch.digits[i] = mask_if(g <= 0) & ((last_four[i] >> 16u) + 1);
kind_1[count_1] = static_cast<std::uint8_t>(i);
count_1 += static_cast<std::size_t>(g == 1);
kind_2[count_2] = static_cast<std::uint8_t>(i);
count_2 += static_cast<std::size_t>(g == 2);
}
// the digits to write, in one loop per kind (see mark())
for (std::size_t k = 0; k != count_1; ++k)
{
const std::size_t i = kind_1[k];
const std::uint64_t s = token_digits_1(digits[i], offset[i], radius[i], last_four[i] & 0xFFu);
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
}
for (std::size_t k = 0; k != count_2; ++k)
{
const std::size_t i = kind_2[k];
const std::uint64_t s = token_digits_2(digits[i], offset[i], radius[i], last_four[i] & 0xFFu, (last_four[i] >> 8u) & 0xFFu);
batch.digits[i] = mark(digits[i], s, last_four[i] >> 16u);
}
return batch.count != 0 && batch.at[0] == n;
}
/*!
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
of room), from the digits of its double (see locate_token()) whose
rounding carried beyond the token's last four digits
(like 43.42027299999999 -> 43.420273): the digits go to the writer of
Zmij's results as its 15 or 16 leading digits and one more, which omits
the trailing zeros.
*/
NLOHMANN_VIEW_NOINLINE char* write_carried_digits(char* w, const node& n, std::uint64_t digits) const noexcept
{
const int exponent = -(n.extra >> 8u);
const std::uint64_t leading = digits / 10;
const auto last = static_cast<unsigned char>(digits - (leading * 10));
const bool seventeen = digits >= 10000000000000000u;
*w = '-';
w += m_doc.src[n.off] == '-' ? 1 : 0;
return ::nlohmann::detail::dtoa_impl::write_shortest(w, seventeen ? ::nlohmann::detail::zmij::shortest_decimal{leading, exponent, last, last != 0}
: ::nlohmann::detail::zmij::shortest_decimal{digits, exponent - 1, 0, false});
}
/// the digits s of a token with the digits w and the last four digits
/// r4, as the batch keeps them: the new last four digits + 1 (only those
/// are written over the token's), or s marked if it differs from w beyond
/// them (0 stays for a float the batch does not write)
static std::uint64_t mark(std::uint64_t w, std::uint64_t s, std::uint64_t r4) noexcept
{
const std::uint64_t last = s - (w - r4);
return last < 10000 ? last + 1 : s | carried();
}
/*!
@brief a float token of 16 or 17 digits as dump() writes it, at w (64 bytes
of room): the token with its last four digits replaced by digits - 1 (see
mark(); the others are the token's), and without trailing zeros after the
point, but one
*/
char* write_token_digits(char* w, const char* src, const node& n, std::uint64_t digits)
{
const char* const token = src + n.off;
std::memcpy(w, token, 32);
char* const end = w + n.len;
const std::uint32_t last = four_digit_text(static_cast<std::uint32_t>(digits - 1));
std::memcpy(end - 4, &last, 4);
// (the token's text before the new digits, from the source: reading
// the output back would wait for the stores)
std::uint64_t before = 0;
std::memcpy(&before, token + n.len - 12, 8);
const std::uint32_t last_zeros = last ^ 0x30303030u;
const std::uint64_t before_zeros = before ^ 0x3030303030303030u;
if (NLOHMANN_VIEW_UNLIKELY(last_zeros == 0 && before_zeros == 0))
{
return write_double_at(w, n); // (twelve or more trailing zeros)
}
const unsigned zeros = last_zeros != 0 ? (static_cast<unsigned>(count_leading_zeros(last_zeros)) / 8) - 4
: 4 + (static_cast<unsigned>(count_leading_zeros(before_zeros)) / 8);
const unsigned frac_digits = n.extra >> 8u;
return end - (zeros < frac_digits ? zeros : frac_digits - 1);
}
#endif
/// a float node as dump() writes it /// a float node as dump() writes it
void write_float_node(const node& n) void write_float_node(const node& n)
{ {
write_float_node(n, fast_double {}); // (only for types other than double: doubles go through write_double_ahead())
}
void write_float_node(const node& n, std::false_type /*other*/)
{
write_float(float_value<number_float_t>(m_doc, n)); write_float(float_value<number_float_t>(m_doc, n));
} }
void write_float_node(const node& n, std::true_type /*double*/)
{
m_out.reserve(64);
m_out.set_cursor(write_double_at(m_out.cursor(), n));
}
/*! /*!
@brief (doubles) the float at n as dump() writes it, at w (64 bytes of room) @brief (doubles) the float at n as dump() writes it, at w (64 bytes of room)
@@ -1095,10 +1487,11 @@ class view_serializer
{ {
const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) const string_t dumped = BasicJsonType(string_t(reinterpret_cast<const char*>(s), n)).dump(); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
static_cast<void>(dumped); static_cast<void>(dumped);
} } // LCOV_EXCL_LINE (dump() throws: the callers pass only ill-formed UTF-8)
template<bool EnsureAscii> /// write the escape sequence of a quote, a backslash or a control
void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len) /// character (the only bytes a run of plain output stops at)
void write_escaped(std::uint32_t codepoint)
{ {
switch (codepoint) switch (codepoint)
{ {
@@ -1126,20 +1519,7 @@ class view_serializer
default: default:
break; break;
} }
if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F)) write_u_escape(codepoint); // (a control character)
{
if (codepoint <= 0xFFFF)
{
write_u_escape(codepoint);
}
else
{
write_u_escape(0xD7C0u + (codepoint >> 10u));
write_u_escape(0xDC00u + (codepoint & 0x3FFu));
}
return;
}
m_out.put(reinterpret_cast<const char*>(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs)
} }
void write_u_escape(std::uint32_t u) void write_u_escape(std::uint32_t u)
+5 -2
View File
@@ -289,9 +289,12 @@ compares, and the UTF-8 check covers the bytes up to it. Returns where the
string scan stops, like scan_string_run: before ill-formed UTF-8 and for the string scan stops, like scan_string_run: before ill-formed UTF-8 and for the
last bytes of the input, the bytes are checked one sequence at a time. Out of last bytes of the input, the bytes are checked one sequence at a time. Out of
line, so that no constants of the check occupy registers in the parse loop. line, so that no constants of the check occupy registers in the parse loop.
On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()). On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()). Not cloned: with
the calls of accept()'s builder, GCC 16 would clone it without plain (always
string_plain()), and calling the clone made json_document::parse() run 0.7-1.2 %
more instructions.
*/ */
NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e, const std::uint8_t* plain) noexcept NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE NLOHMANN_VIEW_NOCLONE inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e, const std::uint8_t* plain) noexcept
{ {
using lookup = utf8_lookup4<>; using lookup = utf8_lookup4<>;
const unsigned char* block = p; const unsigned char* block = p;
+3 -3
View File
@@ -60,7 +60,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod
case value_t::binary: case value_t::binary:
case value_t::discarded: case value_t::discarded:
default: default:
throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind))); throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
} }
} }
@@ -70,7 +70,7 @@ std::vector<U, A> vector_value(const View& v)
{ {
if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) if (NLOHMANN_VIEW_UNLIKELY(!v.is_array()))
{ {
throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name()); throw_type_error(site_of(v), exception_id::type_mismatch, "type must be array, but is ", v.type_name());
} }
std::vector<U, A> r; std::vector<U, A> r;
r.reserve(v.size()); r.reserve(v.size());
@@ -88,7 +88,7 @@ Map map_value(const View& v)
{ {
if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) if (NLOHMANN_VIEW_UNLIKELY(!v.is_object()))
{ {
throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name()); throw_type_error(site_of(v), exception_id::type_mismatch, "type must be object, but is ", v.type_name());
} }
Map r; Map r;
for (auto it = v.begin(); it != v.end(); ++it) for (auto it = v.begin(); it != v.end(); ++it)
+34 -28
View File
@@ -150,7 +150,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// can be restored when json_pointer backwards compatibility is removed // can be restored when json_pointer backwards compatibility is removed
// friend ::nlohmann::json_pointer<StringType>; // friend ::nlohmann::json_pointer<StringType>;
template<typename BasicJsonType, typename InputType> template<typename BasicJsonType, typename InputType, bool DiscardValues>
friend class ::nlohmann::detail::parser; friend class ::nlohmann::detail::parser;
friend ::nlohmann::detail::serializer<basic_json>; friend ::nlohmann::detail::serializer<basic_json>;
template<typename BasicJsonType> template<typename BasicJsonType>
@@ -183,12 +183,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::parser_callback_t<basic_json>cb = nullptr, detail::parser_callback_t<basic_json>cb = nullptr,
const bool allow_exceptions = true, const bool allow_exceptions = true,
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas = false, const bool ignore_trailing_commas = false
const bool discard_number_values = false
) )
{ {
return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter), return ::nlohmann::detail::parser<basic_json, InputAdapterType>(std::move(adapter),
std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas, discard_number_values); std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas);
} }
private: private:
@@ -401,7 +400,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}}; result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}};
#elif defined(__GNUC__) || defined(__GNUG__) #elif defined(__GNUC__) || defined(__GNUG__)
result["compiler"] = {{"family", "gcc"}, {"version", detail::concat<string_t>( result["compiler"] = {{"family", "gcc"}, {"version", detail::concat<string_t>(
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC__)), '.', detail::to_string<string_t>(static_cast<std::size_t>(__GNUC__)), '.', // LCOV_EXCL_LINE
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_MINOR__)), '.', detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_MINOR__)), '.',
detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_PATCHLEVEL__))) detail::to_string<string_t>(static_cast<std::size_t>(__GNUC_PATCHLEVEL__)))
} }
@@ -673,14 +672,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
case value_t::binary: case value_t::binary:
binary = create<binary_t>(); binary = create<binary_t>();
break; break;
case value_t::null: case value_t::null: // LCOV_EXCL_LINE
case value_t::boolean: case value_t::boolean:
case value_t::number_integer: case value_t::number_integer:
case value_t::number_unsigned: case value_t::number_unsigned:
case value_t::number_float: case value_t::number_float:
case value_t::discarded: case value_t::discarded:
default: default:
break; break; // LCOV_EXCL_LINE
} }
} }
@@ -1035,14 +1034,14 @@ public:
destroy_container(t); destroy_container(t);
break; break;
case value_t::null: case value_t::null: // LCOV_EXCL_LINE
case value_t::boolean: case value_t::boolean:
case value_t::number_integer: case value_t::number_integer:
case value_t::number_unsigned: case value_t::number_unsigned:
case value_t::number_float: case value_t::number_float:
case value_t::discarded: case value_t::discarded:
default: default:
break; break; // LCOV_EXCL_LINE
} }
} }
}; };
@@ -1215,9 +1214,9 @@ public:
return is_array() ? array_capacity() : object_capacity(); return is_array() ? array_capacity() : object_capacity();
} }
#else #else
static constexpr std::size_t container_capacity() noexcept static constexpr std::size_t container_capacity() noexcept // LCOV_EXCL_LINE
{ {
return detail::unknown_size(); return detail::unknown_size(); // LCOV_EXCL_LINE
} }
#endif #endif
@@ -1403,7 +1402,7 @@ public:
/// into this switch is worth a few percent when copying a value made /// into this switch is worth a few percent when copying a value made
/// mostly of numbers /// mostly of numbers
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
static void copy_leaf_value(const basic_json& src, basic_json& dst) static void copy_leaf_value(const basic_json& src, basic_json& dst) // LCOV_EXCL_LINE
{ {
switch (src.m_data.m_type) switch (src.m_data.m_type)
{ {
@@ -1577,7 +1576,7 @@ public:
auto src_it = src_object.cbegin(); auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object) for (auto& element : *dst.m_data.m_value.object)
{ {
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) if (JSON_HEDLEY_LIKELY(copy_keys_equal(src_it->first, element.first)))
{ {
copy_shallow(src_it->second, element.second, worklist); copy_shallow(src_it->second, element.second, worklist);
++src_it; ++src_it;
@@ -3117,7 +3116,9 @@ public:
const bool ignore_comments, const bool ignore_comments,
const bool ignore_trailing_commas) const bool ignore_trailing_commas)
{ {
return parser(std::forward<InputAdapterType>(adapter), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); // the parser and lexer that discard the values of tokens (see lexer)
return ::nlohmann::detail::parser<basic_json, typename std::decay<InputAdapterType>::type, true>(std::forward<InputAdapterType>(adapter),
nullptr, false, ignore_comments, ignore_trailing_commas).accept(true);
} }
/// generate SAX events from the input behind an already created adapter /// generate SAX events from the input behind an already created adapter
@@ -5352,8 +5353,7 @@ public:
/// @name lexicographical comparison operators /// @name lexicographical comparison operators
/// @{ /// @{
// the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR // the same-type cases for non-owning scalars, used by JSON_IMPLEMENT_SCALAR_FAST
// and JSON_IMPLEMENT_SCALAR_FAST
#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ #define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \
case value_t::null: \ case value_t::null: \
return (null_result); \ return (null_result); \
@@ -5375,7 +5375,7 @@ public:
// because any negative signed value is smaller than any unsigned value. // because any negative signed value is smaller than any unsigned value.
// Otherwise, the non-negative signed value is cast to unsigned before the // Otherwise, the non-negative signed value is cast to unsigned before the
// comparison to avoid wraparound. // comparison to avoid wraparound.
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \ #define JSON_IMPLEMENT_OPERATOR(op, unordered_result, default_result, deep_result, may_descend) \
const auto lhs_type = lhs.type(); \ const auto lhs_type = lhs.type(); \
const auto rhs_type = rhs.type(); \ const auto rhs_type = rhs.type(); \
\ \
@@ -5403,8 +5403,15 @@ public:
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
} \ } \
\ \
JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ /* same-type scalars never get here: the inline fast path of the operator handles them */ \
\ case value_t::null: \
case value_t::boolean: \
case value_t::number_integer: \
case value_t::number_unsigned: \
case value_t::number_float: \
JSON_ASSERT(false); /* NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) */ \
return (unordered_result); \
\
case value_t::string: \ case value_t::string: \
return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \
\ \
@@ -5444,7 +5451,7 @@ public:
? (number_integer_t(-1) op number_integer_t(1)) \ ? (number_integer_t(-1) op number_integer_t(1)) \
: (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \ : (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \
} \ } \
else if(compares_unordered(lhs, rhs))\ else if(lhs_type == value_t::discarded || rhs_type == value_t::discarded)\
{\ {\
return (unordered_result);\ return (unordered_result);\
}\ }\
@@ -5512,7 +5519,7 @@ public:
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
#endif #endif
JSON_IMPLEMENT_OPERATOR( ==, true, false, false, JSON_IMPLEMENT_OPERATOR( ==, false, false,
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true) compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_POP JSON_HEDLEY_DIAGNOSTIC_POP
@@ -5526,7 +5533,6 @@ public:
// default_result is used if we cannot compare values. In that case, // default_result is used if we cannot compare values. In that case,
// we compare types. // we compare types.
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
std::partial_ordering::equivalent,
std::partial_ordering::unordered, std::partial_ordering::unordered,
lhs_type <=> rhs_type, // *NOPAD* lhs_type <=> rhs_type, // *NOPAD*
to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true) to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
@@ -5538,7 +5544,7 @@ public:
// default_result is used if we cannot compare values. In that case, // default_result is used if we cannot compare values. In that case,
// we compare types. Note we have to call the operator explicitly, // we compare types. Note we have to call the operator explicitly,
// because MSVC has problems otherwise. // because MSVC has problems otherwise.
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), JSON_IMPLEMENT_OPERATOR( <, false, operator<(lhs_type, rhs_type),
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false) compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
} }
#endif #endif
@@ -5553,7 +5559,7 @@ public:
/// their inlined code at each call site small. Binary values are /// their inlined code at each call site small. Binary values are
/// rare and left to equal_general(). /// rare and left to equal_general().
JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_ALWAYS_INLINE
static int equal_leaf(const_reference lhs, const_reference rhs) noexcept static int equal_leaf(const_reference lhs, const_reference rhs) noexcept // LCOV_EXCL_LINE
{ {
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
@@ -6862,7 +6868,7 @@ public:
basic_json result(value_t::object); basic_json result(value_t::object);
json_pointer::flatten("", *this, result); json_pointer::flatten("", *this, result);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief unflatten a previously flattened JSON value /// @brief unflatten a previously flattened JSON value
/// @sa https://json.nlohmann.me/api/basic_json/unflatten/ /// @sa https://json.nlohmann.me/api/basic_json/unflatten/
@@ -7575,7 +7581,7 @@ public:
result.append(path.data(), path.size()); result.append(path.data(), path.size());
diff_append_key(result, key); diff_append_key(result, key);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief the path of the element @a index of the value at @a path /// @brief the path of the element @a index of the value at @a path
static string_t diff_index_path(const string_t& path, const std::size_t index) static string_t diff_index_path(const string_t& path, const std::size_t index)
@@ -7585,7 +7591,7 @@ public:
result.append(path.data(), path.size()); result.append(path.data(), path.size());
diff_append_index(result, index); diff_append_index(result, index);
return result; return result;
} } // LCOV_EXCL_LINE (exception cleanup only)
/// @brief append the "remove" operations for the elements of array /// @brief append the "remove" operations for the elements of array
/// @a source from @a index on, and the "add" operations for the /// @a source from @a index on, and the "add" operations for the
@@ -7745,7 +7751,7 @@ public:
// operations // operations
if (source == target) if (source == target)
{ {
return false; return false; // LCOV_EXCL_LINE (equal objects with the same members in the same order return early)
} }
// slow path: the common keys are in a different relative // slow path: the common keys are in a different relative
+74 -50
View File
@@ -285,7 +285,7 @@ class basic_json_view
{ {
return {}; return {};
} }
detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name());
} }
return lookup(key); return lookup(key);
} }
@@ -311,7 +311,7 @@ class basic_json_view
{ {
return {}; return {};
} }
detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name());
} }
return idx < m_node->len ? element(idx) : basic_json_view(); return idx < m_node->len ? element(idx) : basic_json_view();
} }
@@ -344,12 +344,12 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_object())) if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
{ {
detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
} }
const basic_json_view r = lookup(key); const basic_json_view r = lookup(key);
if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded())) if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded()))
{ {
detail::view::throw_key_not_found(std::string(key.data(), key.size())); detail::view::throw_key_not_found(site(), std::string(key.data(), key.size()));
} }
return r; return r;
} }
@@ -370,11 +370,11 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_array())) if (NLOHMANN_VIEW_UNLIKELY(!is_array()))
{ {
detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
} }
if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len)) if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len))
{ {
detail::view::throw_array_index_out_of_range(idx); detail::view::throw_array_index_out_of_range(site(), idx);
} }
return element(idx); return element(idx);
} }
@@ -402,7 +402,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_object())) if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
{ {
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
} }
const basic_json_view r = lookup(key); const basic_json_view r = lookup(key);
return r.is_discarded() ? default_value : r.template get<T>(); return r.is_discarded() ? default_value : r.template get<T>();
@@ -412,7 +412,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_object())) if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
{ {
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
} }
// (the default is made only if there is no such member) // (the default is made only if there is no such member)
const basic_json_view r = lookup(key); const basic_json_view r = lookup(key);
@@ -427,7 +427,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) if (NLOHMANN_VIEW_UNLIKELY(!is_structured()))
{ {
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
} }
const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value);
return r.is_discarded() ? default_value : r.template get<T>(); return r.is_discarded() ? default_value : r.template get<T>();
@@ -437,7 +437,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) if (NLOHMANN_VIEW_UNLIKELY(!is_structured()))
{ {
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
} }
const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value);
return r.is_discarded() ? string_t(default_value) : r.template get<string_t>(); return r.is_discarded() ? string_t(default_value) : r.template get<string_t>();
@@ -450,7 +450,7 @@ class basic_json_view
const iterator it = begin(); const iterator it = begin();
if (NLOHMANN_VIEW_UNLIKELY(it == end())) if (NLOHMANN_VIEW_UNLIKELY(it == end()))
{ {
detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value"); detail::view::throw_invalid_iterator(site(), detail::exception_id::iterator_value_unavailable, "cannot get value");
} }
return *it; return *it;
} }
@@ -606,7 +606,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_string())) if (NLOHMANN_VIEW_UNLIKELY(!is_string()))
{ {
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be string, but is ", type_name());
} }
return {m_doc->str(*m_node), m_node->len}; return {m_doc->str(*m_node), m_node->len};
} }
@@ -618,7 +618,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_number())) if (NLOHMANN_VIEW_UNLIKELY(!is_number()))
{ {
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
} }
return {m_doc->str(*m_node), detail::view::number_length(*m_node)}; return {m_doc->str(*m_node), detail::view::number_length(*m_node)};
} }
@@ -764,15 +764,19 @@ class basic_json_view
template<typename, view_mode> friend class basic_json_view; template<typename, view_mode> friend class basic_json_view;
template<typename, typename> friend class detail::view::editor; template<typename, typename> friend class detail::view::editor;
template<typename, typename> friend class detail::view::view_side; template<typename, typename> friend class detail::view::view_side;
friend struct detail::view::view_access;
friend iterator; friend iterator;
/// whether the view is one of an editable document
static constexpr bool is_editable = Editable;
basic_json_view(const document_data* d, const node* n) noexcept basic_json_view(const document_data* d, const node* n) noexcept
: m_doc(d), m_node(n) : m_doc(d), m_node(n)
{} {}
/// this value as the context of an exception
detail::view::error_site site() const noexcept
{
return {m_doc, m_node};
}
using json_side_t = detail::view::json_side<BasicJsonType, string_view_t>; using json_side_t = detail::view::json_side<BasicJsonType, string_view_t>;
static detail::view::view_side<BasicJsonType, basic_json_view> side(const basic_json_view& v) noexcept static detail::view::view_side<BasicJsonType, basic_json_view> side(const basic_json_view& v) noexcept
@@ -879,7 +883,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) if (NLOHMANN_VIEW_UNLIKELY(!is_boolean()))
{ {
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name());
} }
return (m_node->flags & detail::view::node_flags::is_true) != 0; return (m_node->flags & detail::view::node_flags::is_true) != 0;
} }
@@ -889,7 +893,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr)) if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr))
{ {
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
} }
return detail::view::arithmetic_value<T, BasicJsonType>(*m_doc, *m_node); return detail::view::arithmetic_value<T, BasicJsonType>(*m_doc, *m_node);
} }
@@ -898,7 +902,7 @@ class basic_json_view
{ {
if (NLOHMANN_VIEW_UNLIKELY(!is_null())) if (NLOHMANN_VIEW_UNLIKELY(!is_null()))
{ {
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be null, but is ", type_name());
} }
return nullptr; return nullptr;
} }
@@ -1063,7 +1067,7 @@ class basic_json_document
const bool ignore_trailing_commas = false) const bool ignore_trailing_commas = false)
{ {
basic_json_document d; basic_json_document d;
d.read_range(first, last, allow_exceptions, ignore_comments, ignore_trailing_commas); d.read_range(first, last, error_mode(allow_exceptions), ignore_comments, ignore_trailing_commas);
return d; return d;
} }
@@ -1078,7 +1082,7 @@ class basic_json_document
basic_json_document d; basic_json_document d;
std::string buf; std::string buf;
append_input(buf, std::forward<InputType>(input)); append_input(buf, std::forward<InputType>(input));
d.build_owned(std::move(buf), allow_exceptions, ignore_comments, ignore_trailing_commas); d.build_owned(std::move(buf), error_mode(allow_exceptions), ignore_comments, ignore_trailing_commas);
return d; return d;
} }
@@ -1091,7 +1095,8 @@ class basic_json_document
static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false) static bool accept(InputType&& input, const bool ignore_comments = false, const bool ignore_trailing_commas = false)
{ {
basic_json_document d; basic_json_document d;
d.read(std::forward<InputType>(input), false, ignore_comments, ignore_trailing_commas); d.read_kind(std::forward<InputType>(input), on_error::validate, ignore_comments, ignore_trailing_commas,
std::integral_constant<detail::view::input_kind, detail::view::classify_input<InputType>::value> {});
return !d.is_discarded(); return !d.is_discarded();
} }
@@ -1107,7 +1112,7 @@ class basic_json_document
const bool ignore_comments = false, const bool ignore_comments = false,
const bool ignore_trailing_commas = false) const bool ignore_trailing_commas = false)
{ {
read_kind(std::forward<InputType>(input), allow_exceptions, ignore_comments, ignore_trailing_commas, read_kind(std::forward<InputType>(input), error_mode(allow_exceptions), ignore_comments, ignore_trailing_commas,
std::integral_constant<detail::view::input_kind, detail::view::classify_input<InputType>::value> {}); std::integral_constant<detail::view::input_kind, detail::view::classify_input<InputType>::value> {});
} }
@@ -1312,7 +1317,7 @@ class basic_json_document
template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0> template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
view_type set(view_type array, I idx, V && value) view_type set(view_type array, I idx, V && value)
{ {
return editor().set(array, index(idx), std::forward<V>(value)); return editor().set(array, index(array, idx), std::forward<V>(value));
} }
/// set the value at a JSON pointer: its parent must exist; an object /// set the value at a JSON pointer: its parent must exist; an object
@@ -1349,7 +1354,7 @@ class basic_json_document
const std::size_t idx = pointer_index(token); const std::size_t idx = pointer_index(token);
if (idx >= 0xFFFFFFFFu) if (idx >= 0xFFFFFFFFu)
{ {
detail::view::throw_array_index_out_of_range(idx); detail::view::throw_array_index_out_of_range(detail::view::site_of(parent), idx);
} }
for (std::size_t i = 0; i < idx; ++i) for (std::size_t i = 0; i < idx; ++i)
{ {
@@ -1383,7 +1388,7 @@ class basic_json_document
template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0> template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
view_type insert(view_type array, I idx, V && value) view_type insert(view_type array, I idx, V && value)
{ {
return editor().insert(array, index(idx), std::forward<V>(value)); return editor().insert(array, index(array, idx), std::forward<V>(value));
} }
/// remove all members with this key; returns their number /// remove all members with this key; returns their number
@@ -1396,7 +1401,7 @@ class basic_json_document
template<typename I, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0> template<typename I, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
void erase(view_type array, I idx) void erase(view_type array, I idx)
{ {
editor().erase(array, index(idx)); editor().erase(array, index(array, idx));
} }
/// remove the value at a JSON pointer; returns the number of removed /// remove the value at a JSON pointer; returns the number of removed
@@ -1430,25 +1435,25 @@ class basic_json_document
return detail::view::editor<BasicJsonType, view_type>(*m_data); return detail::view::editor<BasicJsonType, view_type>(*m_data);
} }
/// an index (out_of_range.401 if negative) /// an index into @a array (out_of_range.401 if negative)
template<typename I> template<typename I>
static std::size_t index(I idx) static std::size_t index(const view_type& array, I idx)
{ {
return index(idx, std::is_signed<I> {}); return index(array, idx, std::is_signed<I> {});
} }
template<typename I> template<typename I>
static std::size_t index(I idx, std::true_type /*signed*/) static std::size_t index(const view_type& array, I idx, std::true_type /*signed*/)
{ {
if (idx < 0) if (idx < 0)
{ {
detail::view::throw_array_index_out_of_range(idx); detail::view::throw_array_index_out_of_range(detail::view::site_of(array), idx);
} }
return static_cast<std::size_t>(idx); return static_cast<std::size_t>(idx);
} }
template<typename I> template<typename I>
static std::size_t index(I idx, std::false_type /*unsigned*/) static std::size_t index(const view_type& /*array*/, I idx, std::false_type /*unsigned*/)
{ {
return static_cast<std::size_t>(idx); return static_cast<std::size_t>(idx);
} }
@@ -1466,6 +1471,22 @@ class basic_json_document
return idx; return idx;
} }
/// what a failed parse does
enum class on_error : std::uint8_t
{
discard, ///< the document is discarded
raise, ///< parse_error is thrown
validate ///< accept(): the document is discarded, and no node is stored while parsing
};
static on_error error_mode(bool allow_exceptions) noexcept
{
return allow_exceptions ? on_error::raise : on_error::discard;
}
/// NulIsEnd of the builder: a null byte ends the input unless strict_nul_handling
static constexpr bool nul_is_end = !detail::abi_config::strict_nul_handling;
/// create the storage (sized for the input) on first use /// create the storage (sized for the input) on first use
void ensure_data(const char* src, std::size_t size) void ensure_data(const char* src, std::size_t size)
{ {
@@ -1477,7 +1498,7 @@ class basic_json_document
/// parse a buffer the document takes ownership of (it replaces the /// parse a buffer the document takes ownership of (it replaces the
/// previous owned copy) /// previous owned copy)
void build_owned(std::string&& buf, bool allow_exceptions, bool comments, bool trailing_commas) void build_owned(std::string&& buf, on_error allow_exceptions, bool comments, bool trailing_commas)
{ {
ensure_data(buf.data(), buf.size()); ensure_data(buf.data(), buf.size());
m_data->owned = std::move(buf); m_data->owned = std::move(buf);
@@ -1487,7 +1508,7 @@ class basic_json_document
/// parse a copy of the input; the copy is appended to the cleared owned /// parse a copy of the input; the copy is appended to the cleared owned
/// buffer, so that its capacity is reused /// buffer, so that its capacity is reused
template<typename T> template<typename T>
void build_copy(T&& input, bool allow_exceptions, bool comments, bool trailing_commas) void build_copy(T&& input, on_error allow_exceptions, bool comments, bool trailing_commas)
{ {
if (!m_data) if (!m_data)
{ {
@@ -1508,7 +1529,7 @@ class basic_json_document
} }
/// sentinel: src[size] is readable and 0 (std::string, C strings) /// sentinel: src[size] is readable and 0 (std::string, C strings)
void build(const char* src, std::size_t size, bool allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel) void build(const char* src, std::size_t size, on_error allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel)
{ {
ensure_data(src, size); ensure_data(src, size);
document_data& d = *m_data; document_data& d = *m_data;
@@ -1535,7 +1556,8 @@ class basic_json_document
} }
else else
{ {
ok = detail::view::build < typename BasicJsonType::number_float_t, !detail::abi_config::strict_nul_handling > (d, src, size, comments, trailing_commas, sentinel, failure); ok = detail::view::build<typename BasicJsonType::number_float_t, nul_is_end>(d, src, size, comments, trailing_commas, sentinel,
allow_exceptions == on_error::validate, failure);
} }
if (NLOHMANN_VIEW_LIKELY(ok)) if (NLOHMANN_VIEW_LIKELY(ok))
{ {
@@ -1548,7 +1570,7 @@ class basic_json_document
return; return;
} }
std::vector<std::uint32_t>().swap(d.large_objects); std::vector<std::uint32_t>().swap(d.large_objects);
if (allow_exceptions) if (allow_exceptions == on_error::raise)
{ {
detail::view::throw_parse_failure<BasicJsonType>(failure, src, size, comments, trailing_commas); detail::view::throw_parse_failure<BasicJsonType>(failure, src, size, comments, trailing_commas);
} }
@@ -1556,13 +1578,13 @@ class basic_json_document
// --- input dispatch (see detail::view::input_kind) --- // --- input dispatch (see detail::view::input_kind) ---
void read_kind(std::string&& s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::move_string> /*unused*/) void read_kind(std::string&& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::move_string> /*unused*/)
{ {
build_owned(std::move(s), ae, c, tc); build_owned(std::move(s), ae, c, tc);
} }
template<typename CharT> template<typename CharT>
void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::c_string> /*unused*/) void read_kind(CharT* s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::c_string> /*unused*/)
{ {
if (s == nullptr) if (s == nullptr)
{ {
@@ -1576,7 +1598,7 @@ class basic_json_document
} }
template<typename Array> template<typename Array>
void read_kind(Array& a, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::char_array> /*unused*/) void read_kind(Array& a, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::char_array> /*unused*/)
{ {
using CharT = typename std::remove_cv<typename std::remove_extent<Array>::type>::type; using CharT = typename std::remove_cv<typename std::remove_extent<Array>::type>::type;
const std::size_t n = std::extent<Array>::value; const std::size_t n = std::extent<Array>::value;
@@ -1587,7 +1609,7 @@ class basic_json_document
} }
template<typename T> template<typename T>
void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::borrow_range> /*unused*/) void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::borrow_range> /*unused*/)
{ {
// std::basic_string guarantees data()[size()] == 0: use it as sentinel // std::basic_string guarantees data()[size()] == 0: use it as sentinel
const auto size = static_cast<std::size_t>(s.size()); const auto size = static_cast<std::size_t>(s.size());
@@ -1596,13 +1618,13 @@ class basic_json_document
} }
template<typename T> template<typename T>
void read_kind(const T& s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::copy_range> /*unused*/) void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::copy_range> /*unused*/)
{ {
build_copy(s, ae, c, tc); build_copy(s, ae, c, tc);
} }
template<typename CharT> template<typename CharT>
void read_kind(CharT* s, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::wide_c_string> /*unused*/) void read_kind(CharT* s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::wide_c_string> /*unused*/)
{ {
if (s == nullptr) if (s == nullptr)
{ {
@@ -1614,13 +1636,13 @@ class basic_json_document
} }
template<typename T> template<typename T>
void read_kind(T&& input, bool ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::adapter> /*unused*/) void read_kind(T&& input, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::adapter> /*unused*/)
{ {
build_copy(std::forward<T>(input), ae, c, tc); build_copy(std::forward<T>(input), ae, c, tc);
} }
template<typename IteratorType> template<typename IteratorType>
void read_range(IteratorType first, IteratorType last, bool ae, bool c, bool tc) void read_range(IteratorType first, IteratorType last, on_error ae, bool c, bool tc)
{ {
using value_type = typename std::remove_cv<typename std::iterator_traits<IteratorType>::value_type>::type; using value_type = typename std::remove_cv<typename std::iterator_traits<IteratorType>::value_type>::type;
// borrow the range where the library's input adapter scans it as one // borrow the range where the library's input adapter scans it as one
@@ -1631,14 +1653,14 @@ class basic_json_document
} }
template<typename IteratorType> template<typename IteratorType>
void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::true_type /*contiguous bytes*/) void read_range_impl(IteratorType first, IteratorType last, on_error ae, bool c, bool tc, std::true_type /*contiguous bytes*/)
{ {
const auto size = static_cast<std::size_t>(std::distance(first, last)); const auto size = static_cast<std::size_t>(std::distance(first, last));
build(size == 0 ? "" : reinterpret_cast<const char*>(&*first), size, ae, c, tc, false, size == 0); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) build(size == 0 ? "" : reinterpret_cast<const char*>(&*first), size, ae, c, tc, false, size == 0); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
} }
template<typename IteratorType> template<typename IteratorType>
void read_range_impl(IteratorType first, IteratorType last, bool ae, bool c, bool tc, std::false_type /*other*/) void read_range_impl(IteratorType first, IteratorType last, on_error ae, bool c, bool tc, std::false_type /*other*/)
{ {
build_copy(detail::view::make_adapter_input(detail::input_adapter(first, last)), ae, c, tc); build_copy(detail::view::make_adapter_input(detail::input_adapter(first, last)), ae, c, tc);
} }
@@ -1683,9 +1705,11 @@ class basic_json_document
{ {
// (as before: the library's adapter decides what a null stream yields) // (as before: the library's adapter decides what a null stream yields)
detail::view::append_adapter(buf, detail::input_adapter(file)); detail::view::append_adapter(buf, detail::input_adapter(file));
return;
} }
detail::view::append_file(buf, file); else
{
detail::view::append_file(buf, file);
}
} }
#endif #endif
+11880
View File
File diff suppressed because it is too large. Load diff
+1487 -552
View File
File diff suppressed because it is too large. Load diff
+1656 -619
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -15,6 +15,7 @@
#include <nlohmann/json_view.hpp> #include <nlohmann/json_view.hpp>
#include <algorithm> // any_of #include <algorithm> // any_of
#include <cstddef> // size_t
#include <functional> // function #include <functional> // function
#include <random> // mt19937 #include <random> // mt19937
#include <string> // string #include <string> // string
@@ -88,6 +89,27 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
}); });
} }
// the message of an exception about the value at path (a JSON pointer, "" for
// the root) whose source bytes are [start, end): the context basic_json adds
// with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, if enabled, then what
inline std::string message(const std::string& id, const std::string& path, std::size_t start, std::size_t end, const std::string& what)
{
#if JSON_DIAGNOSTIC_POSITIONS
const std::string positions = "(bytes " + std::to_string(start) + "-" + std::to_string(end) + ") ";
#else
const std::string positions;
static_cast<void>(start);
static_cast<void>(end);
#endif
#if JSON_DIAGNOSTICS
const std::string context = path.empty() ? "" : "(" + path + ") " + positions;
#else
const std::string& context = positions;
static_cast<void>(path);
#endif
return "[json.exception." + id + "] " + context + what;
}
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none // the exception a call throws, or "" if it throws none
inline std::string exception_of_call(const std::function<void()>& f) inline std::string exception_of_call(const std::function<void()>& f)
+68
View File
@@ -14,7 +14,9 @@ using nlohmann::json;
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <memory>
#include <sstream> #include <sstream>
#include <stdexcept>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -767,3 +769,69 @@ TEST_CASE("a write that throws leaves the same bytes in every kind of output")
} }
} }
#endif #endif
#if !defined(JSON_NOEXCEPTION)
namespace
{
// an adapter whose every write fails
class failing_adapter : public nlohmann::detail::output_adapter_protocol<char>
{
public:
[[noreturn]] void write_character(char /*c*/) override
{
throw std::runtime_error("write failed");
}
[[noreturn]] void write_characters(const char* /*s*/, std::size_t /*length*/) override
{
throw std::runtime_error("write failed");
}
};
} // namespace
#endif
TEST_CASE("output adapters")
{
SECTION("writing a single character")
{
// the writers pass whole buffers on, but an adapter takes single
// characters as well
std::vector<char> as_vector;
nlohmann::detail::output_vector_adapter<char> vector_adapter(as_vector);
vector_adapter.write_character('a');
vector_adapter.write_character('b');
CHECK(as_vector == std::vector<char>({'a', 'b'}));
std::ostringstream as_stream;
nlohmann::detail::output_stream_adapter<char> stream_adapter(as_stream);
stream_adapter.write_character('c');
stream_adapter.write_character('d');
CHECK(as_stream.str() == "cd");
std::string as_string;
nlohmann::detail::output_string_adapter<char> string_adapter(as_string);
string_adapter.write_character('e');
string_adapter.write_character('f');
CHECK(as_string == "ef");
}
#if !defined(JSON_NOEXCEPTION)
SECTION("a sink that is destroyed with characters left over")
{
// the destructor passes the pending characters on, as a write that
// threw leaves them there; the adapter failing then must not escape
{
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
sink.write_character('x');
}
// while flush() reports the failure
nlohmann::detail::output_adapter_sink<char> sink(std::make_shared<failing_adapter>());
sink.write_character('x');
CHECK_THROWS_AS(sink.flush(), std::runtime_error&);
// (the character counts as written, so there is nothing left for the destructor)
}
#endif
}
+103
View File
@@ -26,11 +26,14 @@ using nlohmann::ordered_json;
#include <limits> #include <limits>
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <list>
#include <set> #include <set>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#include "sax_countdown.hpp" #include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown; using utils::SaxCountdown;
using utils::SaxEventLogger;
// trait_test_arg and the "value_in_range_of trait" TEST_CASE_TEMPLATE_DEFINE // trait_test_arg and the "value_in_range_of trait" TEST_CASE_TEMPLATE_DEFINE
@@ -2309,6 +2312,23 @@ TEST_CASE("BJData")
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata)); CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
} }
SECTION("every event of an ndarray of chars")
{
// the elements of an ndarray of 'C' are read one by one
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c', 'd'};
// start_object, key, string, key, start_array, 2 numbers, end_array,
// key, start_array, 4 chars, end_array, end_object
constexpr int events = 16;
for (int allowed = 0; allowed < events; ++allowed)
{
CAPTURE(allowed)
SaxCountdown scp(allowed);
CHECK_FALSE(json::sax_parse(v, &scp, json::input_format_t::bjdata));
}
SaxCountdown scp(events);
CHECK(json::sax_parse(v, &scp, json::input_format_t::bjdata));
}
SECTION("start_array() in ndarray _ArrayData_") SECTION("start_array() in ndarray _ArrayData_")
{ {
std::vector<uint8_t> const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1}; std::vector<uint8_t> const v = {'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 6, 5, 4, 3, 2, 1};
@@ -3092,6 +3112,15 @@ TEST_CASE("BJData")
json _; json _;
CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82"); CHECK_THROWS_WITH(_ = json::from_bjdata(v), "[json.exception.parse_error.113] parse error at byte 2: syntax error while parsing BJData char: byte after 'C' must be in range 0x00..0x7F; last byte: 0x82");
} }
SECTION("ndarray of chars cut off by the end of the input")
{
// 2x2 chars announced, three present
std::vector<uint8_t> const v = {'[', '$', 'C', '#', '[', '$', 'U', '#', 'U', 2, 2, 2, 'a', 'b', 'c'};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(v), "[json.exception.parse_error.110] parse error at byte 16: syntax error while parsing BJData char: unexpected end of input", json::parse_error&);
CHECK(json::from_bjdata(v, true, false).is_discarded());
}
} }
SECTION("byte") SECTION("byte")
@@ -4746,3 +4775,77 @@ TEST_CASE("BJData optimized container type marker is validated for count 0")
CHECK(json::from_bjdata(vb) == json::binary({})); CHECK(json::from_bjdata(vb) == json::binary({}));
} }
} }
TEST_CASE("BJData optimized arrays from contiguous and non-contiguous input")
{
// contiguous input reads the numbers of an array with a type marker in
// one loop, other input number by number; both must give the same events,
// value, or error for the complete document and for every truncation of
// it, and stop at every event a SAX handler rejects
json document =
{
{"u8", {0, 200, 255}},
{"i8", {-5, 100, -128}},
{"i16", {-30000, 1000, 30000}},
{"i32", {-2000000000, 5, 70000}},
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
{"f64", {1.5, -0.1, 2.5}},
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
{"mixed", {1, "a", 2.5, nullptr}},
{"u16", {40000, 1, 65535}},
{"u32", {4000000000U, 1, 70000}},
{"u64", {18446744073709551615ULL, 1, 5000000000ULL}},
{"nd", {{"_ArrayType_", "double"}, {"_ArraySize_", {2, 2}}, {"_ArrayData_", {1.5, -2.5, 3.5, -4.5}}}},
{"nd8", {{"_ArrayType_", "uint8"}, {"_ArraySize_", {3}}, {"_ArrayData_", {1, 2, 3}}}},
};
const std::vector<std::uint8_t> encoded = json::to_bjdata(document, true, true);
CHECK(json::from_bjdata(encoded) == document);
for (std::size_t length = 0; length <= encoded.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_bjdata(contiguous, true, false).dump() == json::from_bjdata(bytewise.begin(), bytewise.end(), true, false).dump());
#if !defined(JSON_NOEXCEPTION)
std::string contiguous_error;
std::string bytewise_error;
try
{
json _;
_ = json::from_bjdata(contiguous);
}
catch (const json::exception& e)
{
contiguous_error = e.what();
}
try
{
json _;
_ = json::from_bjdata(bytewise.begin(), bytewise.end());
}
catch (const json::exception& e)
{
bytewise_error = e.what();
}
CHECK(contiguous_error == bytewise_error);
#endif
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::bjdata)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 80; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::bjdata)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::bjdata));
}
}
+3
View File
@@ -329,6 +329,9 @@ TEST_CASE("BON8")
check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9}); check_bon8({false, nullptr, true}, {0x83, 0xF8, 0xFA, 0xF9});
check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD}); check_bon8({false, nullptr, true, 1.0}, {0x84, 0xF8, 0xFA, 0xF9, 0xFD});
check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE}); check_bon8({false, nullptr, true, 1.0, json::array(), 0.0}, {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE});
// an array that starts with a number: -1, 0, and 1 have bytes of their own
check_bon8({-1.0, 0.0, 1.0}, {0x83, 0xFB, 0xFC, 0xFD});
check_bon8({1.0, -1.0, -0.0}, {0x83, 0xFD, 0xFB, 0x8E, 0x80, 0x00, 0x00, 0x00});
check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91}); check_bon8({{{1}}}, {0x81, 0x81, 0x81, 0x91});
check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF}); check_bon8({{{"foo"}}}, {0x81, 0x81, 0x81, 'f', 'o', 'o', 0xFF});
check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF}); check_bon8({{{""}}}, {0x81, 0x81, 0x81, 0xFF});
+261 -2
View File
@@ -19,6 +19,7 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
#include <array>
#include <cstdint> #include <cstdint>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
@@ -80,8 +81,20 @@ class huge_string_t : public std::string
return result; return result;
} }
// returns a copy of @a s whose size() pretends to be @a n
static huge_string_t as_size(const std::string& s, const size_type n)
{
huge_string_t result(s);
result.fake_size = n;
return result;
}
size_type size() const noexcept size_type size() const noexcept
{ {
if (fake_size != 0)
{
return fake_size;
}
if (pretend_huge) if (pretend_huge)
{ {
// one byte more than the BSON length field can represent // one byte more than the BSON length field can represent
@@ -92,6 +105,7 @@ class huge_string_t : public std::string
private: private:
bool pretend_huge = false; bool pretend_huge = false;
size_type fake_size = 0;
}; };
using huge_string_json = nlohmann::json::with_string_t<huge_string_t>; using huge_string_json = nlohmann::json::with_string_t<huge_string_t>;
@@ -266,6 +280,17 @@ TEST_CASE("BSON")
CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&); CHECK_THROWS_WITH_AS(huge_string_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_string_json::out_of_range&);
} }
SECTION("an array element that no longer fits the document")
{
// the string fits the length field, but not the document that
// holds it; nothing of it is written
const auto limit = static_cast<std::string::size_type>((std::numeric_limits<std::int32_t>::max)());
huge_string_json j;
j["a"] = huge_string_json::array({huge_string_t::as_size("value", limit - 8)});
CHECK_THROWS_AS(huge_string_json::to_bson(j), huge_string_json::out_of_range&);
}
SECTION("document") SECTION("document")
{ {
// an oversized string nested one level deep makes the // an oversized string nested one level deep makes the
@@ -897,6 +922,38 @@ TEST_CASE("BSON")
CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&); CHECK_THROWS_AS(_ = json::from_bson(input), json::parse_error&);
} }
SECTION("binary member with subtype 0x02 (old binary) with a truncated inner length")
{
// the inner length field is cut off by the end of the input
std::vector<std::uint8_t> const input =
{
0x0F, 0x00, 0x00, 0x00, // size (little endian)
0x05, 'a', 0x00, // entry: binary
0x08, 0x00, 0x00, 0x00, // size of binary (little endian)
0x02, // "old binary" subtype
0x01 // first byte of the inner length
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON number: unexpected end of input", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("string member that is cut off by the end of the input")
{
std::vector<std::uint8_t> const input =
{
0x0F, 0x00, 0x00, 0x00, // size (little endian)
0x02, 'a', 0x00, // entry: string
0x0A, 0x00, 0x00, 0x00, // size of the string (little endian)
'b', 'c' // the string ends early
};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BSON string: unexpected end of input", json::parse_error&);
CHECK(json::from_bson(input, true, false).is_discarded());
}
SECTION("Some more complex document") SECTION("Some more complex document")
{ {
json const j = json const j =
@@ -1164,7 +1221,13 @@ TEST_CASE("BSON keys from contiguous and stream input")
// contiguous input reads a key up to its \x00-byte in one step, a stream // contiguous input reads a key up to its \x00-byte in one step, a stream
// reads it byte by byte; both must give the same value or error for the // reads it byte by byte; both must give the same value or error for the
// complete document and for every truncation of it // complete document and for every truncation of it
const json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}}; json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}};
// keys and array element names ("0" to "11") that end at every position of
// an 8-byte word, which contiguous input scans at a time
for (std::size_t length = 1; length <= 17; ++length)
{
j[std::string(length, static_cast<char>('a' + length))] = json::array({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, {11}});
}
const std::vector<std::uint8_t> bson = json::to_bson(j); const std::vector<std::uint8_t> bson = json::to_bson(j);
CHECK(json::from_bson(bson) == j); CHECK(json::from_bson(bson) == j);
@@ -1184,7 +1247,7 @@ TEST_CASE("BSON keys from contiguous and stream input")
} }
try try
{ {
std::istringstream stream(std::string(input.begin(), input.end())); std::istringstream stream(std::string(reinterpret_cast<const char*>(input.data()), input.size()));
from_stream = json::from_bson(stream).dump(); from_stream = json::from_bson(stream).dump();
} }
catch (const json::parse_error& e) catch (const json::parse_error& e)
@@ -1194,6 +1257,33 @@ TEST_CASE("BSON keys from contiguous and stream input")
CHECK(from_vector == from_stream); CHECK(from_vector == from_stream);
} }
} }
TEST_CASE("BSON array element names")
{
// an array's element names are not part of the value, so they are not
// passed on; error_handler_t::strict still rejects ill-formed UTF-8 in them
const std::vector<std::uint8_t> bson =
{
0x1D, 0x00, 0x00, 0x00, // size (little endian)
0x04, 'a', 0x00, // entry: array "a"
0x15, 0x00, 0x00, 0x00, // size (little endian)
0x08, 0xFF, 0x00, 0x01, // entry: boolean named "\xFF", true
0x08, '1', '2', '3', '4', '5', '6', '7', '8', '9', 0x00, 0x00, // entry: boolean named "123456789", false
0x00, // end of the array
0x00 // end of the document
};
const json expected = {{"a", {true, false}}};
CHECK(json::from_bson(bson) == expected);
CHECK(json::from_bson(bson, true, true, json::error_handler_t::replace) == expected);
CHECK(json::from_bson(bson, true, true, json::error_handler_t::ignore) == expected);
json _;
CHECK_THROWS_WITH_AS(_ = json::from_bson(bson, true, true, json::error_handler_t::strict), "[json.exception.parse_error.113] parse error at byte 14: syntax error while parsing BSON key: invalid string: ill-formed UTF-8 byte", json::parse_error&);
std::istringstream stream(std::string(reinterpret_cast<const char*>(bson.data()), bson.size()));
CHECK(json::from_bson(stream) == expected);
}
#endif #endif
TEST_CASE("Negative size of binary value") TEST_CASE("Negative size of binary value")
@@ -2102,6 +2192,25 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
CHECK(output.empty()); CHECK(output.empty());
} }
#if !defined(JSON_NOEXCEPTION)
SECTION("U+0000 in a long key is found at any position")
{
// the key is searched eight characters at a time
for (const std::string& key :
{
std::string("abc\0defghijkl", 12), std::string("abcdefgh\0ijkl", 13), std::string("abcdefghijklmnop\0", 17), std::string("\0abcdefghijkl", 13),
std::string("abcdefghijklmno\0", 16)
})
{
CAPTURE(key)
const json value = json::object({{key, 1}});
std::vector<std::uint8_t> output;
CHECK_THROWS_AS(json::to_bson(value, output), json::out_of_range&);
CHECK(output.empty());
}
}
#endif
SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)") SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)")
{ {
// the offending value is nested, so this also covers that the check // the offending value is nested, so this also covers that the check
@@ -2267,3 +2376,153 @@ TEST_CASE("BSON large strings and binaries (chunked reader)")
} }
} }
} }
TEST_CASE("BSON written in one pass and in two passes")
{
// to_bson() into a returned vector writes each size once the object or
// array is complete; through an output adapter, all sizes are computed
// first. Both must produce the same bytes and throw the same exceptions.
const auto two_pass = [](const json & j, const json::error_handler_t handler)
{
std::vector<std::uint8_t> result;
json::to_bson(j, result, handler);
return result;
};
const auto check_same = [&](const json & j, const json::error_handler_t handler)
{
CAPTURE(j.dump(-1, ' ', true, json::error_handler_t::replace))
CAPTURE(static_cast<int>(handler))
#if !defined(JSON_NOEXCEPTION)
std::string one_pass_error;
std::string two_pass_error;
std::vector<std::uint8_t> one;
std::vector<std::uint8_t> two;
try
{
one = json::to_bson(j, handler);
}
catch (const json::exception& e)
{
one_pass_error = e.what();
}
try
{
two = two_pass(j, handler);
}
catch (const json::exception& e)
{
two_pass_error = e.what();
}
CHECK(one_pass_error == two_pass_error);
CHECK(one == two);
#else
CHECK(json::to_bson(j, handler) == two_pass(j, handler));
#endif
};
const std::array<json::error_handler_t, 4> handlers = {{
json::error_handler_t::keep, json::error_handler_t::strict,
json::error_handler_t::replace, json::error_handler_t::ignore
}
};
SECTION("corpus")
{
for (const auto& value : utils::round_trip_corpus::values())
{
const json document = value.is_object() ? value : json{{"value", value}};
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
}
SECTION("nesting and arrays of every length class")
{
json document = json::object();
document["empty object"] = json::object();
document["empty array"] = json::array();
for (const std::size_t n :
{
1u, 9u, 10u, 11u, 99u, 100u, 101u, 1000u
})
{
json numbers = json::array();
json mixed = json::array();
for (std::size_t i = 0; i < n; ++i)
{
numbers.push_back(i % 3 == 0 ? json(static_cast<double>(i) / 3) : json(static_cast<std::int64_t>(i) * 1000003));
switch (i % 4)
{
case 0:
mixed.push_back(json("s" + std::to_string(i)));
break;
case 1:
mixed.push_back(json::array({i, json::object({{"k", i}})}));
break;
case 2:
mixed.push_back(json(nullptr));
break;
default:
mixed.push_back(json(i % 2 == 0));
break;
}
}
document["numbers " + std::to_string(n)] = numbers;
document["mixed " + std::to_string(n)] = mixed;
document["nested " + std::to_string(n)] = json::object({{"a", numbers}, {"b", json::array({mixed, json::object()})}});
}
document["binary"] = json::binary({1, 2, 3}, 0x80);
document["binary old"] = json::binary({1, 2, 3}, 0x02);
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
#if !defined(JSON_NOEXCEPTION)
// every entry here throws, which aborts without exceptions
SECTION("errors at every position")
{
// an entry that cannot be written, at each place among valid ones
const std::string bad_utf8 = "\xC3\x28";
const std::array<json, 7> bad_entries =
{
{
json::object({{std::string("a\0b", 3), 1}}),
json::object({{"s", bad_utf8}}),
json::object({{bad_utf8, 1}}),
json::array({1, 2, bad_utf8}),
json::object({{"b", json::binary({1}, 0x100)}}),
json::object({{"d", json(json::value_t::discarded)}}),
json::array({json(json::value_t::discarded)}),
}
};
for (const auto& bad : bad_entries)
{
for (std::size_t pos = 0; pos < 4; ++pos)
{
json document = json::object();
json list = json::array();
for (std::size_t i = 0; i < 4; ++i)
{
if (i == pos)
{
document["x" + std::to_string(i)] = bad;
list.push_back(bad);
}
document["n" + std::to_string(i)] = json::array({i, "str", json::object({{"k", i}})});
list.push_back(i);
}
document["list"] = list;
for (const auto handler : handlers)
{
check_same(document, handler);
}
}
}
}
#endif
}
+98
View File
@@ -29,7 +29,9 @@ using nlohmann::json;
#include "round_trip_corpus.hpp" #include "round_trip_corpus.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#include "sax_countdown.hpp" #include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown; using utils::SaxCountdown;
using utils::SaxEventLogger;
TEST_CASE("CBOR") TEST_CASE("CBOR")
@@ -1807,6 +1809,30 @@ TEST_CASE("CBOR")
CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector<uint8_t>({0xa1, 0xff, 0x01}), true, false).is_discarded());
} }
SECTION("tag in front of a key that is cut off by the end of the input")
{
// 0xD8: the tag number is in the next byte, which is missing
const std::vector<std::uint8_t> input = {0xA1, 0xD8};
json _;
for (const auto handler :
{
json::cbor_tag_handler_t::ignore, json::cbor_tag_handler_t::store
})
{
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input, true, true, handler), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false, handler).is_discarded());
}
}
SECTION("chunk of an indefinite-length string that is cut off by the end of the input")
{
// the second byte announces a chunk of three bytes, but two follow
const std::vector<std::uint8_t> input = {0x7F, 0x63, 'a', 'b'};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false).is_discarded());
}
SECTION("tag in front of a key") SECTION("tag in front of a key")
{ {
const auto ignore = json::cbor_tag_handler_t::ignore; const auto ignore = json::cbor_tag_handler_t::ignore;
@@ -2446,6 +2472,78 @@ TEST_CASE("CBOR SAX parsing stops at every event")
CHECK(count_events(std::vector<std::uint8_t>({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6); CHECK(count_events(std::vector<std::uint8_t>({0xBF, 0x61, 'a', 0x9F, 0x01, 0xFF, 0xFF})) == 6);
} }
TEST_CASE("CBOR arrays of scalars from contiguous and non-contiguous input")
{
// contiguous input reads the integers, floats, booleans, and nulls an
// array begins with in one loop, other input value by value; both must
// give the same events, value, or error for the complete document and for
// every truncation of it, and stop at every event a SAX handler rejects
const std::vector<std::uint8_t> cbor =
{
0xA3, // map(3)
0x61, 'a', // "a"
0x98, 0x16, // array(22)
0x00, 0x17, 0x18, 0xFF, 0x19, 0x01, 0x00, // 0, 23, 255, 256
0x1A, 0x00, 0x01, 0x00, 0x00, // 65536
0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
0x20, 0x37, 0x38, 0xFF, 0x39, 0x01, 0x00, // -1, -24, -256, -257
0x3A, 0x00, 0x01, 0x00, 0x00, // -65537
0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -18446744073709551616 (a float)
0x3B, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -9223372036854775808
0xF4, 0xF5, 0xF6, // false, true, null
0xFA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float32)
0xFB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
0xF9, 0x3C, 0x00, // 1.0 (float16, ends the loop)
0x01, 0x61, 'a', // 1, "a"
0xFB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
0x61, 'b', // "b"
0x83, // array(3)
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x82, 0xFB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xFB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x80, // []
0x61, 'c', // "c"
0x84, 0xA0, 0x01, 0xC1, 0x02, 0x81, 0x03 // [{}, 1, tag(1) 2, [3]]
};
const auto tags = json::cbor_tag_handler_t::ignore;
const json j = json::from_cbor(cbor, true, true, tags);
CHECK(j["a"].size() == 22);
CHECK(j["a"][5] == 18446744073709551615u);
CHECK(j["a"][11].is_number_float());
CHECK(j["a"][11] == -18446744073709551616.0);
CHECK(j["a"][12] == (std::numeric_limits<std::int64_t>::min)());
CHECK(j["a"][17] == -0.1);
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
CHECK(j["c"] == json::parse("[{}, 1, 2, [3]]"));
for (std::size_t length = 0; length <= cbor.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(cbor.begin(), cbor.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_cbor(contiguous, true, false, tags).dump() == json::from_cbor(bytewise.begin(), bytewise.end(), true, false, tags).dump());
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 60; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(cbor.begin(), cbor.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(cbor, &from_contiguous, json::input_format_t::cbor, true, false, false, tags)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::cbor, true, false, false, tags));
}
}
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk") TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
{ {
// Reading an indefinite-length string or byte array used to call itself // Reading an indefinite-length string or byte array used to call itself
+75
View File
@@ -2092,6 +2092,12 @@ TEST_CASE("UTF-8 validators accept the same sequences")
} }
}; };
// an ASCII byte is a sequence of its own
for (unsigned lead = 0; lead < 0x80; ++lead)
{
CHECK(nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead)).length == 1);
}
for (unsigned lead = 0x80; lead <= 0xFF; ++lead) for (unsigned lead = 0x80; lead <= 0xFF; ++lead)
{ {
const auto shape = nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead)); const auto shape = nlohmann::detail::utf8_shape(static_cast<std::uint8_t>(lead));
@@ -2157,3 +2163,72 @@ TEST_CASE("UTF-8 validators accept the same sequences")
} }
} }
} }
TEST_CASE("float_bigint")
{
// the big integer of the exact comparison of a float token with the
// midpoint between two floats (digit_comparison())
using nlohmann::detail::float_bigint;
SECTION("zero")
{
// shifting zero keeps it zero, whatever the distance
float_bigint zero(0);
zero.shift_left(0);
zero.shift_left(5);
zero.shift_left(64);
CHECK(zero.compare(float_bigint(0)) == 0);
CHECK(zero.compare(float_bigint(1)) == -1);
CHECK(float_bigint(1).compare(zero) == 1);
}
SECTION("compare numbers of different lengths")
{
// the number with more 32-bit limbs is the greater one
const float_bigint small(0xFFFFFFFFu);
const float_bigint large(std::uint64_t{1} << 32u);
CHECK(small.compare(large) == -1);
CHECK(large.compare(small) == 1);
CHECK(large.compare(large) == 0);
// a shift can add a limb
float_bigint shifted(1);
shifted.shift_left(32);
CHECK(shifted.compare(large) == 0);
shifted.shift_left(1);
CHECK(shifted.compare(large) == 1);
CHECK(large.compare(shifted) == -1);
}
SECTION("compare numbers of the same length")
{
CHECK(float_bigint(2).compare(float_bigint(3)) == -1);
CHECK(float_bigint(3).compare(float_bigint(2)) == 1);
CHECK(float_bigint(3).compare(float_bigint(3)) == 0);
CHECK(float_bigint((std::uint64_t{3} << 32u) | 1u).compare(float_bigint((std::uint64_t{3} << 32u) | 2u)) == -1);
}
}
TEST_CASE("serialization of short strings")
{
// the serializer takes strings of up to 32 characters that need no
// escaping by a table lookup of its own where the scan is not vectorized
// (this file is built that way, too, with JSON_NO_SIMD)
for (const std::string& s :
{
std::string(), std::string("a"), std::string("hello"), std::string("0123456789abcdef"), std::string("0123456789abcdef0123456789abcdef"),
std::string("0123456789abcdef0123456789abcdef0")
})
{
CAPTURE(s)
const json j = s;
CHECK(j.dump() == "\"" + s + "\"");
CHECK(j.dump(-1, ' ', true) == "\"" + s + "\"");
CHECK(json({{s, s}}).dump() == "{\"" + s + "\":\"" + s + "\"}");
}
// a character that needs an escape
CHECK(json("a\"b").dump() == "\"a\\\"b\"");
CHECK(json("caf\xC3\xA9").dump() == "\"caf\xC3\xA9\"");
CHECK(json("caf\xC3\xA9").dump(-1, ' ', true) == "\"caf\\u00e9\"");
}
+61
View File
@@ -2982,6 +2982,67 @@ TEST_CASE("accept() agrees with parse() on floats around the overflow limit")
} }
} }
TEST_CASE("accept() agrees with parse() on strings, which it checks without copying them")
{
// accept() scans strings and object keys with a separate instantiation of
// the string scanner that does not copy them into the token buffer; it
// must accept and reject exactly what parse() does
const std::vector<std::string> pieces =
{
"", "abc", R"(\"\\\/\b\f\n\r\t)", R"(\u0041\u00e4\u20AC)", "\\ud83d\\ude00", "\\uD83D\\uDE00",
"\\ud800", "\\udc00", "\\ud800\\u0041", "\\ud800x", "\\udbff\\udfff", "\\u12", "\\u12g4", "\\x", "\\",
"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf", "\xc0\xaf", "\xed\xa0\x80", "\xf4\x90\x80\x80",
"\xff", "\xe2\x82", "\xc3", "\x01", "\x1f", "\x7f", "\n", std::string(1, '\0')
};
for (std::size_t run = 0; run <= 70; ++run)
{
for (const std::string& piece : pieces)
{
// a run of ordinary characters puts the piece at every offset of the bulk scanners
const std::string str = std::string(run, 'a') + piece;
std::string opened = "\"";
opened += str;
std::string closed = opened;
closed += '"';
std::vector<std::string> texts;
texts.push_back(closed);
texts.emplace_back("[");
texts.back() += closed;
texts.back() += ']';
texts.emplace_back("{");
texts.back() += closed;
texts.back() += ":1}";
texts.emplace_back("{\"k\":");
texts.back() += closed;
texts.back() += ',';
texts.back() += closed;
texts.back() += ":[";
texts.back() += closed;
texts.back() += "]}";
texts.emplace_back("[");
texts.back() += opened;
for (const std::string& text : texts)
{
CAPTURE(text)
for (int options = 0; options < 4; ++options)
{
const bool ignore_comments = (options & 1) != 0;
const bool ignore_trailing_commas = (options & 2) != 0;
const bool parses = !json::parse(text, nullptr, false, ignore_comments, ignore_trailing_commas).is_discarded();
CHECK(json::accept(text, ignore_comments, ignore_trailing_commas) == parses);
std::istringstream stream(text);
CHECK(json::accept(stream, ignore_comments, ignore_trailing_commas) == parses);
const std::list<char> chars(text.begin(), text.end());
CHECK(json::accept(chars.begin(), chars.end(), ignore_comments, ignore_trailing_commas) == parses);
}
}
}
}
}
namespace namespace
{ {
// a DOM parser that reports parse errors without throwing, as in the sax_no_exception example // a DOM parser that reports parse errors without throwing, as in the sax_no_exception example
+20
View File
@@ -210,3 +210,23 @@ TEST_CASE("regression test #5476 - array type without reserve()")
CHECK(json::from_cbor(truncated, true, false).is_discarded()); CHECK(json::from_cbor(truncated, true, false).is_discarded());
} }
} }
TEST_CASE("array type with std::deque nested beyond the recursion limit")
{
// the binary writers finish a value nested deeper than
// recursion_depth_limit() with an explicit stack of frames; with the
// iterators of std::deque, these frames are copied and moved through
// their own special members
deque_json j = 1;
for (int i = 0; i < 200; ++i)
{
deque_json outer = deque_json::array();
outer.push_back(std::move(j));
j = std::move(outer);
}
// 200 arrays of one element each (0x81) around the integer 1
std::vector<std::uint8_t> expected(200, 0x81);
expected.push_back(0x01);
CHECK(deque_json::to_cbor(j) == expected);
}
+21
View File
@@ -49,3 +49,24 @@ TEST_CASE("custom object key types: MessagePack (json_full)")
{ {
custom_key_test::test_msgpack<json_full>(); custom_key_test::test_msgpack<json_full>();
} }
TEST_CASE("custom object key types: patch (json_full)")
{
// the members of a patch are found by lookup, as their keys are not std::string
const json_full j = json_full::parse(R"({"a":1,"b":[1,2],"c":{"d":2}})");
const json_full patch = json_full::parse(R"([
{"op": "add", "path": "/e", "value": 5},
{"op": "remove", "path": "/a"},
{"op": "replace", "path": "/c/d", "value": 3},
{"op": "test", "path": "/c/d", "value": 3},
{"op": "move", "from": "/e", "path": "/f"},
{"op": "copy", "from": "/f", "path": "/g"},
{"op": "add", "path": "/b/-", "value": 7}
])");
CHECK(j.patch(patch).dump() == R"({"b":[1,2,7],"c":{"d":3},"f":5,"g":5})");
// a patch with a missing member is rejected
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "add", "path": "/e"}])")), json_full::parse_error&);
CHECK_THROWS_AS(j.patch(json_full::parse(R"([{"op": "test", "path": "/a", "value": 2}])")), json_full::other_error&);
}
+66
View File
@@ -337,6 +337,52 @@ TEST_CASE("custom number types: integers beyond 64 bits")
CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64)); CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64));
} }
SECTION("MessagePack integers of every width")
{
// an integer of a number type wider than 64 bits is written with the
// smallest MessagePack integer that holds it, as for 64-bit types
const wide_json widths = wide_json::array(
{
make_int(-33), make_int(-100), make_int(-128), make_int(-129), make_int(-32768), make_int(-32769), make_int(-2147483648LL), make_int(-2147483649LL),
make_int(128), make_int(255), make_int(256), make_int(300), make_int(65535), make_int(65536), make_int(70000), make_int(4294967295LL), make_int(4294967296LL),
make_uint(128), make_uint(255), make_uint(256), make_uint(300), make_uint(65535), make_uint(65536), make_uint(70000), make_uint(4294967295ULL), make_uint(4294967296ULL)
});
const nlohmann::json widths64 = nlohmann::json::array(
{
-33, -100, -128, -129, -32768, -32769, -2147483648LL, -2147483649LL,
128, 255, 256, 300, 65535, 65536, 70000, 4294967295LL, 4294967296LL,
128u, 255u, 256u, 300u, 65535u, 65536u, 70000u, 4294967295ULL, 4294967296ULL
});
CHECK(wide_json::to_msgpack(widths) == nlohmann::json::to_msgpack(widths64));
// as single values, too
CHECK(wide_json::to_msgpack(make_int(-100)) == std::vector<std::uint8_t>({0xD0, 0x9C}));
CHECK(wide_json::to_msgpack(make_int(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
CHECK(wide_json::to_msgpack(make_int(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
CHECK(wide_json::to_msgpack(make_uint(300)) == std::vector<std::uint8_t>({0xCD, 0x01, 0x2C}));
CHECK(wide_json::to_msgpack(make_uint(70000)) == std::vector<std::uint8_t>({0xCE, 0x00, 0x01, 0x11, 0x70}));
}
SECTION("dump")
{
// beyond 64 bits, the digits are counted before they are written
CHECK(make_int(0).dump() == "0");
CHECK(make_int(two_64).dump() == "18446744073709551616");
CHECK(make_int(-two_64).dump() == "-18446744073709551616");
CHECK(make_int(two_100).dump() == "1267650600228229401496703205376");
CHECK(make_int(-two_100).dump() == "-1267650600228229401496703205376");
// powers of ten, where the count of digits ends on a single digit or two
const int128 ten_20 = static_cast<int128>(100000000000ULL) * static_cast<int128>(1000000000ULL);
CHECK(make_int(ten_20).dump() == "100000000000000000000");
CHECK(make_int(-ten_20).dump() == "-100000000000000000000");
CHECK(make_int(ten_20 * 10).dump() == "1000000000000000000000");
CHECK(make_int(ten_20 * 100).dump() == "10000000000000000000000");
CHECK(make_int(ten_20 * 1000).dump() == "100000000000000000000000");
CHECK(make_uint(static_cast<uint128>(ten_20) * 10u).dump() == "1000000000000000000000");
CHECK(make_uint((std::numeric_limits<uint128>::max)()).dump() == "340282366920938463463374607431768211455");
CHECK(make_int((std::numeric_limits<int128>::min)()).dump() == "-170141183460469231731687303715884105728");
}
SECTION("BJData ND-array") SECTION("BJData ND-array")
{ {
// an ND-array element beyond 64 bits does not fit any dtype, so the // an ND-array element beyond 64 bits does not fit any dtype, so the
@@ -417,3 +463,23 @@ TEST_CASE("custom number types")
} }
} }
TEST_CASE("custom number types: 32-bit integers in the binary formats")
{
// integers narrower than 64 bits are widened (a signed one with its sign)
// before the CBOR, MessagePack, and UBJSON writers pick their encoding
using json32 = nlohmann::json::with_integers_t<std::int32_t, std::uint32_t>;
json32 values = json32::array();
values.push_back(std::uint32_t{0});
values.push_back(std::uint32_t{200});
values.push_back(std::uint32_t{4294967295u});
values.push_back(std::int32_t{-1});
values.push_back(std::int32_t{-2147483647 - 1});
REQUIRE(values[2].is_number_unsigned());
REQUIRE(values[4].is_number_integer());
const nlohmann::json values64 = {0u, 200u, 4294967295u, -1, -2147483648LL};
CHECK(json32::to_cbor(values) == nlohmann::json::to_cbor(values64));
CHECK(json32::to_msgpack(values) == nlohmann::json::to_msgpack(values64));
CHECK(json32::to_ubjson(values) == nlohmann::json::to_ubjson(values64));
}
+163
View File
@@ -11,8 +11,13 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <string> #include <string>
#include <type_traits>
#include <utility> #include <utility>
#include <vector>
#include "custom_object_key_type.hpp" #include "custom_object_key_type.hpp"
@@ -125,3 +130,161 @@ TEST_CASE("MessagePack supports custom object key types nested deeper than the r
CHECK(encoded == nlohmann::json::to_msgpack(expected)); CHECK(encoded == nlohmann::json::to_msgpack(expected));
CHECK(nlohmann::json::from_msgpack(encoded) == expected); CHECK(nlohmann::json::from_msgpack(encoded) == expected);
} }
namespace
{
// A key type that cannot be constructed from a std::string, so that a value
// with such keys cannot be converted from a nlohmann::json by the range
// constructor of its object type: the serializer converts the object instead.
class strict_key
{
public:
strict_key(const char* value)
: m_value(value)
{}
strict_key(const std::string& /*value*/) = delete;
#if JSON_DIAGNOSTICS
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
// when building the path of an exception.
const char* data() const noexcept
{
return m_value.data();
}
#endif
friend bool operator<(const strict_key& lhs, const strict_key& rhs)
{
return lhs.m_value < rhs.m_value;
}
private:
std::string m_value{};
};
strict_key make_strict_key(const std::string& key)
{
return strict_key(key.c_str());
}
template<class Key, class Value, class Compare, class Allocator>
class strict_object
: public std::map <
strict_key,
Value,
std::less<strict_key>, // NOLINT(modernize-use-transparent-functors)
typename std::allocator_traits<Allocator>::template rebind_alloc <
std::pair<const strict_key, Value >>>
{
public:
strict_object() = default;
};
template<typename T, typename = void>
struct strict_serializer : nlohmann::adl_serializer<T> {};
using strict_json = nlohmann::basic_json < strict_object, std::vector, std::string, bool, std::int64_t,
std::uint64_t, double, std::allocator, strict_serializer, std::vector<std::uint8_t >>;
// the members of an object are converted one by one, their keys by make_strict_key()
template<>
struct strict_serializer<nlohmann::json::object_t, void>
{
static void to_json(strict_json& j, const nlohmann::json::object_t& object)
{
strict_json::object_t converted;
for (const auto& member : object)
{
converted.emplace(make_strict_key(member.first), strict_json(member.second));
}
j = strict_json(std::move(converted));
}
};
} // namespace
TEST_CASE("converting a value with keys that cannot be made from a std::string nested deeper than the recursion depth limit")
{
static_assert(!std::is_constructible<strict_key, const std::string&>::value, "the keys cannot be made from a std::string");
// below detail::recursion_depth_limit(), objects and arrays are converted
// by the serializers instead of the range constructors
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10;
nlohmann::json value = 1;
for (std::size_t i = 0; i < depth; ++i)
{
if (i % 2 == 0)
{
value = nlohmann::json::object({{"a", std::move(value)}, {"b", true}});
}
else
{
value = nlohmann::json::array({std::move(value), "sibling"});
}
}
const strict_json converted = value;
// at() needs to make a std::string of a key for its error message
const auto member = [](const strict_json & object, const char* name) -> const strict_json &
{
const auto it = object.find(strict_key(name));
REQUIRE(it != object.end());
return *it;
};
// walk down the nesting levels without recursion
const strict_json* current = &converted;
for (std::size_t i = depth; i > 0; --i)
{
if (i % 2 == 1)
{
REQUIRE(current->is_object());
CHECK(current->size() == 2);
CHECK(member(*current, "b").get<bool>());
current = &member(*current, "a");
}
else
{
REQUIRE(current->is_array());
CHECK(current->size() == 2);
CHECK(current->at(1).get<std::string>() == "sibling");
current = &current->at(0);
}
}
CHECK(current->get<int>() == 1);
}
namespace
{
// shorter keys come first
struct shortlex_less
{
bool operator()(const std::string& lhs, const std::string& rhs) const
{
return lhs.size() != rhs.size() ? lhs.size() < rhs.size() : lhs < rhs;
}
};
template<class Key, class Value, class Compare, class Allocator>
using shortlex_map = std::map<Key, Value, shortlex_less, Allocator>;
using shortlex_json = nlohmann::json::with_object_t<shortlex_map>;
} // namespace
TEST_CASE("unflatten with an object type that does not sort the keys alphabetically")
{
// the keys of /a come before and after one of /b: unlike in an object
// sorted alphabetically, the values below a prefix are not found one
// after the other, so the value of /a must be looked up again
shortlex_json flat = shortlex_json::object();
flat["/a/p"] = 1;
flat["/b/qq"] = 2;
flat["/a/pqr"] = 3;
REQUIRE(flat.begin().key() == "/a/p");
REQUIRE((++flat.begin()).key() == "/b/qq");
CHECK(flat.unflatten().dump() == R"({"a":{"p":1,"pqr":3},"b":{"qq":2}})");
CHECK(flat.unflatten().flatten() == flat);
}
+22
View File
@@ -1195,6 +1195,28 @@ TEST_CASE("deserialization")
#endif #endif
} }
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
TEST_CASE("deserialization from a pointer and a length (deprecated)")
{
// the unit test build disables -Wdeprecated-declarations (see
// cmake/clang_flags.cmake)
const std::string text = R"({"foo": [1, 2, 3]})";
const std::string invalid = R"({"foo": )";
SECTION("parse")
{
const json j = json::parse({text.data(), text.size()});
CHECK(j == json({{"foo", {1, 2, 3}}}));
}
SECTION("accept")
{
CHECK(json::accept({text.data(), text.size()}));
CHECK_FALSE(json::accept({invalid.data(), invalid.size()}));
}
}
#endif
// select the types to test - char8_t is only available since C++20 if and only // select the types to test - char8_t is only available since C++20 if and only
// if __cpp_char8_t is defined. // if __cpp_char8_t is defined.
#define TYPE_LIST(...) __VA_ARGS__ #define TYPE_LIST(...) __VA_ARGS__
+20
View File
@@ -868,3 +868,23 @@ TEST_CASE("Parent pointers stay consistent for object types not known to keep th
obj["1"].push_back(42); obj["1"].push_back(42);
CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&); CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&);
} }
TEST_CASE("patch_inplace() with a patch that is stored inside the patched value")
{
// the patch is modified-while-iterated if it is not copied first: it is a
// descendant of the value it is applied to, which only the parent pointers
// (JSON_DIAGNOSTICS) reveal
json j =
{
{"patch", {{{"op", "add"}, {"path", "/x"}, {"value", 1}}, {{"op", "remove"}, {"path", "/patch/0"}}}},
{"y", 2}
};
j.patch_inplace(j["patch"]);
CHECK(j.size() == 3);
CHECK(j["x"] == 1);
CHECK(j["y"] == 2);
CHECK(j["patch"].size() == 1);
CHECK(j["patch"][0]["op"] == "remove");
}
+1
View File
@@ -403,6 +403,7 @@ TEST_CASE("guard page")
CHECK(nlohmann::detail::find_string_special(p, n) <= n); CHECK(nlohmann::detail::find_string_special(p, n) <= n);
CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n); CHECK(nlohmann::detail::find_ascii_copyable_run(p, n) <= n);
CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n); CHECK(nlohmann::detail::valid_utf8_prefix(p, n) <= n);
CHECK(nlohmann::detail::valid_utf8_string(p, n) == (nlohmann::detail::valid_utf8_prefix(p, n) == n));
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17) #if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n); CHECK(nlohmann::detail::find_string_delimiter(p, n) <= n);
#endif #endif
+19
View File
@@ -18,6 +18,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
#include <iterator>
#if JSON_HAS_RANGES #if JSON_HAS_RANGES
#include <algorithm> #include <algorithm>
#include <ranges> #include <ranges>
@@ -665,6 +667,23 @@ TEST_CASE("iterators 2")
} }
} }
SECTION("reverse iterators of a const value")
{
const json j_array = {1, 2, 3};
const json j_object = {{"one", 1}, {"two", 2}};
// rbegin() and rend() of a const value are crbegin() and crend()
CHECK(j_array.rbegin() == j_array.crbegin());
CHECK(j_array.rend() == j_array.crend());
CHECK(*j_array.rbegin() == 3);
CHECK(std::distance(j_array.rbegin(), j_array.rend()) == 3);
CHECK(j_object.rbegin() == j_object.crbegin());
CHECK(j_object.rend() == j_object.crend());
CHECK(j_object.rbegin().key() == "two");
CHECK(std::distance(j_object.rbegin(), j_object.rend()) == 2);
}
SECTION("reverse iterator arithmetic") SECTION("reverse iterator arithmetic")
{ {
json j_object = {{"one", 1}, {"two", 2}, {"three", 3}}; json j_object = {{"one", 1}, {"two", 2}, {"three", 3}};
+98
View File
@@ -14,9 +14,11 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif #endif
#include <cstddef>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
@@ -2453,3 +2455,99 @@ TEST_CASE("JSON patch - diff behavior documented in the API reference")
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json); CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
} }
} }
TEST_CASE("JSON patch - operations that are left to json_pointer")
{
// the usual operations on a plain pointer are applied without building a
// json_pointer; the following are not plain, and some are errors
SECTION("a path without a slash")
{
const json j = {{"a", 1}};
CHECK_THROWS_AS(j.patch(R"([{"op": "add", "path": "abc", "value": 1}])"_json), json::parse_error&);
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "abc"}])"_json), json::parse_error&);
}
SECTION("remove with an escaped reference token")
{
const json j = {{"a/b", 1}, {"c~d", 2}, {"e", 3}};
CHECK(j.patch(R"([{"op": "remove", "path": "/a~1b"}])"_json) == json({{"c~d", 2}, {"e", 3}}));
CHECK(j.patch(R"([{"op": "remove", "path": "/c~0d"}])"_json) == json({{"a/b", 1}, {"e", 3}}));
CHECK_THROWS_AS(j.patch(R"([{"op": "remove", "path": "/missing~1key"}])"_json), json::out_of_range&);
}
SECTION("a path through a primitive value")
{
const json j = {{"a", 1}};
CHECK_THROWS_AS(j.patch(R"([{"op": "test", "path": "/a/b", "value": 1}])"_json), json::other_error&);
CHECK_THROWS_AS(j.patch(R"([{"op": "replace", "path": "/a/b", "value": 1}])"_json), json::out_of_range&);
}
}
TEST_CASE("JSON patch - diff of objects with many members")
{
// more members than diff() keeps without allocating
for (const std::size_t members :
{
16u, 17u, 40u
})
{
CAPTURE(members)
json source = json::object();
json target = json::object();
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = "key" + std::to_string(i);
source[key] = i;
// every third member differs
target[key] = (i % 3 == 0) ? i + 1 : i;
}
// the same keys, a removed and an added one
source["removed"] = true;
target["added"] = false;
const json patch = json::diff(source, target);
CHECK(source.patch(patch) == target);
CHECK(patch.size() == ((members + 2) / 3) + 2);
const nlohmann::ordered_json ordered_source = source;
const nlohmann::ordered_json ordered_target = target;
CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target);
}
}
TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive descent")
{
// the values at the innermost level are diffed without recursion: equal
// leaves yield nothing, leaves of different types and different leaves
// are replaced
const std::size_t depth = 200;
json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr};
json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr};
for (std::size_t i = 0; i < depth; ++i)
{
source = json::array({std::move(source)});
target = json::array({std::move(target)});
}
const json patch = json::diff(source, target);
CHECK(patch.size() == 3);
CHECK(source.patch(patch) == target);
// the same with objects as the outer levels
json object_source = {{"equal", 1}, {"type", "text"}, {"value", 3}};
json object_target = {{"equal", 1}, {"type", 2}, {"value", 4}};
for (std::size_t i = 0; i < depth; ++i)
{
object_source = json::object({{"a", std::move(object_source)}});
object_target = json::object({{"a", std::move(object_target)}});
}
const json object_patch = json::diff(object_source, object_target);
CHECK(object_patch.size() == 2);
CHECK(object_source.patch(object_patch) == object_target);
}
+16
View File
@@ -106,6 +106,12 @@ TEST_CASE("JSON pointers")
CHECK_THROWS_WITH_AS(p.pop_back(), CHECK_THROWS_WITH_AS(p.pop_back(),
"[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
// the top of a pointer is its first reference token
const json::json_pointer nonempty("/foo/bar/0");
CHECK(nonempty.top() == json::json_pointer("/foo"));
CHECK(nonempty.top().to_string() == "/foo");
CHECK(json::json_pointer("/a~1b").top() == json::json_pointer("/a~1b"));
SECTION("array index error") SECTION("array index error")
{ {
json v = {1, 2, 3, 4}; json v = {1, 2, 3, 4};
@@ -697,6 +703,16 @@ TEST_CASE("JSON pointers")
CHECK_THROWS_WITH_AS(j_error.unflatten(), CHECK_THROWS_WITH_AS(j_error.unflatten(),
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&); "[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
// error for an invalid escape sequence in a key
CHECK_THROWS_WITH_AS(json({{"/a~2", 1}}).unflatten(),
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
CHECK_THROWS_WITH_AS(json({{"/a/b", 1}, {"/c~", 1}}).unflatten(),
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
// error for an element of an array that is also a container
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/0/x", 2}}).unflatten(),
"[json.exception.type_error.313] invalid value to unflatten", json::type_error&);
// explicit roundtrip check // explicit roundtrip check
CHECK(j.flatten().unflatten() == j); CHECK(j.flatten().unflatten() == j);
+90 -72
View File
@@ -31,9 +31,11 @@ using json_view_test::has_duplicate_keys;
#include <ios> #include <ios>
#include <istream> #include <istream>
#include <iterator> #include <iterator>
#include <functional>
#include <limits> #include <limits>
#include <list> #include <list>
#include <map> #include <map>
#include <new>
#include <random> #include <random>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -98,6 +100,37 @@ template<typename View>
using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>())); using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>()));
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
// an allocator that never has room
template<typename T>
struct failing_allocator
{
using value_type = T;
failing_allocator() = default;
template<typename U>
explicit failing_allocator(const failing_allocator<U>& /*other*/) noexcept {}
T* allocate(std::size_t /*n*/) // NOLINT(readability-convert-member-functions-to-static)
{
throw std::bad_alloc();
}
void deallocate(T* /*p*/, std::size_t /*n*/) noexcept {} // NOLINT(readability-convert-member-functions-to-static)
template<typename U>
bool operator==(const failing_allocator<U>& /*other*/) const noexcept
{
return true;
}
template<typename U>
bool operator!=(const failing_allocator<U>& /*other*/) const noexcept
{
return false;
}
};
// the exception parse() throws for a text, or "" if it accepts it // the exception parse() throws for a text, or "" if it accepts it
std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false) std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
{ {
@@ -225,7 +258,7 @@ TEST_CASE("json_view")
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// the parents are set, so errors name the path // the parents are set, so errors name the path
const json m = materialized<json_document>(R"({"a":{"b":[1]}})"); const json m = materialized<json_document>(R"({"a":{"b":[1]}})");
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&); CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), json_view_test::message("type_error.304", "/a/b/0", 11, 12, "cannot use at() with number").c_str(), json::type_error&);
#endif #endif
} }
@@ -235,7 +268,9 @@ TEST_CASE("json_view")
{ {
"", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e",
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal) "\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal)
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}" "1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}",
// (an error after every kind of value: the error is found by replaying the parser)
"[null, true, -1, 1, 1.5, \"s\", {\"a\": 1} 2]", "{\"a\": null, \"b\": -1, \"c\": 1.5, \"d\": \"s\", \"e\": {}, 1}"
}) })
{ {
CAPTURE(text) CAPTURE(text)
@@ -368,6 +403,10 @@ TEST_CASE("json_view")
CHECK(from_stream.root().materialize() == expected); CHECK(from_stream.root().materialize() == expected);
const std::list<char> list(text.begin(), text.end()); const std::list<char> list(text.begin(), text.end());
CHECK(json_document::parse(list.begin(), list.end()).owns_source()); CHECK(json_document::parse(list.begin(), list.end()).owns_source());
// (an input that is not valid)
const std::list<char> bad_list{'[', '1', ','};
json_document from_bad_list;
CHECK_THROWS_AS(from_bad_list = json_document::parse(bad_list.begin(), bad_list.end()), json::parse_error&);
CHECK(materialized<json_document>(list.begin(), list.end()) == expected); CHECK(materialized<json_document>(list.begin(), list.end()) == expected);
// iterator pairs: pointers are borrowed, and so are contiguous library // iterator pairs: pointers are borrowed, and so are contiguous library
@@ -750,19 +789,6 @@ std::string exception_of(F f)
return ""; return "";
} }
// the exception a call on a basic_json value throws, without the byte range of
// the value (JSON_DIAGNOSTIC_POSITIONS), which the exceptions of views lack
template<typename F>
std::string json_exception_of(F f)
{
std::string what = exception_of(f);
const std::size_t at = what.find("] (bytes ");
if (at != std::string::npos)
{
what.erase(at + 2, what.find(") ", at) - at);
}
return what;
}
#endif #endif
// compares a view with the ordered_json value materialize() gives for it: // compares a view with the ordered_json value materialize() gives for it:
@@ -971,24 +997,25 @@ TEST_CASE("json_view element access and iteration")
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
if (!j.is_object()) if (!j.is_object())
{ {
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == json_exception_of([&] { static_cast<void>(j["a"]); })); CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
} }
if (!j.is_array()) if (!j.is_array())
{ {
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == json_exception_of([&] { static_cast<void>(j[0]); })); CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
} }
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == json_exception_of([&] { static_cast<void>(j.at("a")); })); CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == exception_of([&] { static_cast<void>(j.at("a")); }));
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == json_exception_of([&] { static_cast<void>(j.at("missing")); })); CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == json_exception_of([&] { static_cast<void>(j.at(0)); })); CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == exception_of([&] { static_cast<void>(j.at(0)); }));
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == json_exception_of([&] { static_cast<void>(j.at(5)); })); CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
if (!(j.is_object() && j.empty())) // (key() of an end iterator) if (!(j.is_object() && j.empty())) // (key() of an end iterator)
{ {
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == json_exception_of([&] { static_cast<void>(j.begin().key()); })); // (without the context of basic_json: view iterators do not know the value they iterate over)
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == (j.is_object() ? "" : "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators"));
} }
if (!j.empty() || j.is_null()) if (!j.empty() || j.is_null())
{ {
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == json_exception_of([&] { static_cast<void>(j.front()); })); CHECK(exception_of([&] { static_cast<void>(v.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == json_exception_of([&] { static_cast<void>(j.back()); })); CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
} }
#endif #endif
CHECK(v.contains("a") == j.contains("a")); CHECK(v.contains("a") == j.contains("a"));
@@ -1000,8 +1027,8 @@ TEST_CASE("json_view element access and iteration")
const json_document d = json_document::parse(R"({"a":[]})"); const json_document d = json_document::parse(R"({"a":[]})");
CHECK(d.root()["b"].is_discarded()); CHECK(d.root()["b"].is_discarded());
CHECK(d.root()["a"][0].is_discarded()); CHECK(d.root()["a"][0].is_discarded());
CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.root()["a"].front(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.root()["a"].back(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
const json_view invalid{}; const json_view invalid{};
CHECK(invalid.begin() == invalid.end()); CHECK(invalid.begin() == invalid.end());
CHECK(std::string(invalid.type_name()) == "discarded"); CHECK(std::string(invalid.type_name()) == "discarded");
@@ -1032,10 +1059,10 @@ TEST_CASE("json_view element access and iteration")
CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded()); CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded());
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
// type errors on values that are not discarded stay // type errors on values that are not discarded stay
CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&); CHECK_THROWS_WITH_AS(v[0], json_view_test::message("type_error.305", "", 0, 29, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&); CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], json_view_test::message("type_error.305", "/a/b", 10, 17, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&); CHECK_THROWS_WITH_AS(v["s"]["c"], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a string argument with string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&); CHECK_THROWS_WITH_AS(v["s"][0], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a numeric argument with string").c_str(), json::type_error&);
CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&); CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&);
CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&); CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&);
// at() keeps throwing on a discarded view // at() keeps throwing on a discarded view
@@ -1110,9 +1137,9 @@ TEST_CASE("json_view element access and iteration")
CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded()); CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded());
CHECK(v[std::numeric_limits<int>::max()].is_discarded()); CHECK(v[std::numeric_limits<int>::max()].is_discarded());
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(v.at(3), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(v.at(3u), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_AS(v.at(-1), json::out_of_range&); CHECK_THROWS_AS(v.at(-1), json::out_of_range&);
CHECK_THROWS_AS(v.at(-1L), json::out_of_range&); CHECK_THROWS_AS(v.at(-1L), json::out_of_range&);
CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&); CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&);
@@ -1246,26 +1273,6 @@ TEST_CASE("json_view element access and iteration")
namespace namespace
{ {
#if !defined(JSON_NOEXCEPTION)
// an exception message without the context that basic_json adds with
// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) ");
// the view's exceptions have no such context
std::string without_path(std::string msg)
{
for (const char* prefix :
{"] (/", "] (bytes "
})
{
const std::size_t open = msg.find(prefix);
if (open != std::string::npos)
{
msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2));
}
}
return msg;
}
#endif
// the bits of a float, to compare values bit for bit // the bits of a float, to compare values bit for bit
std::uint64_t bits(double x) std::uint64_t bits(double x)
{ {
@@ -1336,18 +1343,18 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
// conversions to the wrong type throw what basic_json throws // conversions to the wrong type throw what basic_json throws
if (!j.is_number()) if (!j.is_number())
{ {
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<int>()); }))); CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == exception_of([&] { static_cast<void>(j.get<int>()); }));
} }
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<bool>()); }))); CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == exception_of([&] { static_cast<void>(j.get<bool>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::string>()); }))); CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == exception_of([&] { static_cast<void>(j.get<std::string>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); }))); CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); }));
if (!j.is_array()) if (!j.is_array())
{ {
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); }))); CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); }));
} }
if (!j.is_object()) if (!j.is_object())
{ {
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); }))); CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); }));
} }
#endif #endif
@@ -1490,8 +1497,8 @@ TEST_CASE("json_view values")
CHECK(v[3].number_token() == "123456789012345678901234567890"); CHECK(v[3].number_token() == "123456789012345678901234567890");
CHECK(v[4].number_token() == "-12"); CHECK(v[4].number_token() == "-12");
CHECK(v[5].number_token() == "7"); CHECK(v[5].number_token() == "7");
CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&); CHECK_THROWS_WITH_AS(v[6].number_token(), json_view_test::message("type_error.302", "/6", 56, 59, "type must be number, but is string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&); CHECK_THROWS_WITH_AS(v.get_string(), json_view_test::message("type_error.302", "", 0, 60, "type must be string, but is array").c_str(), json::type_error&);
} }
SECTION("conversions") SECTION("conversions")
@@ -1543,6 +1550,17 @@ TEST_CASE("json_view values")
const json_document dup = json_document::parse(R"({"a":1,"a":2})"); const json_document dup = json_document::parse(R"({"a":1,"a":2})");
CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}})); CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}}));
// a map of views from something that is not an object
using view_map = std::map<std::string, json_view>;
const json_document not_an_object = json_document::parse("[1,2]");
CHECK_THROWS_WITH_AS(not_an_object.root().get<view_map>(), json_view_test::message("type_error.302", "", 0, 5, "type must be object, but is array").c_str(), json::type_error&);
#if !defined(JSON_NOEXCEPTION)
// (and one that cannot allocate its nodes)
using failing_map = std::map<std::string, json_view, std::less<std::string>, failing_allocator<std::pair<const std::string, json_view>>>;
const json_document one_member = json_document::parse(R"({"a":1})");
CHECK_THROWS_AS(one_member.root().get<failing_map>(), std::bad_alloc&);
#endif
const json_view invalid{}; const json_view invalid{};
CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&);
CHECK(invalid.get<json>().is_discarded()); CHECK(invalid.get<json>().is_discarded());
@@ -1565,15 +1583,15 @@ TEST_CASE("json_view values")
CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0)); CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0));
CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3)); CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3));
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); }))); CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast<void>(j.value("s", 0)); }))); CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == exception_of([&] { static_cast<void>(j.value("s", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", 0)); }))); CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == exception_of([&] { static_cast<void>(j["n"].value("x", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); }))); CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); }));
// string defaults: the member is not a string, or the value is neither an object nor an array // string defaults: the member is not a string, or the value is neither an object nor an array
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value("n", "default")); }))); CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == exception_of([&] { static_cast<void>(j.value("n", "default")); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", "default")); }))); CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == exception_of([&] { static_cast<void>(j["n"].value("x", "default")); }));
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); }))); CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); }));
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); }))); CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); }));
#endif #endif
// (defaults and members longer than the short string optimization) // (defaults and members longer than the short string optimization)
CHECK(v.value("s", "a default longer than fifteen characters") == "text"); CHECK(v.value("s", "a default longer than fifteen characters") == "text");
@@ -1632,7 +1650,7 @@ TEST_CASE("json_view JSON pointers")
{ {
CAPTURE(pointer) CAPTURE(pointer)
const json::json_pointer p(pointer); const json::json_pointer p(pointer);
const std::string at_error = without_path(exception_of([&] { static_cast<void>(j.at(p)); })); const std::string at_error = exception_of([&] { static_cast<void>(j.at(p)); });
CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error); CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error);
if (at_error.empty()) if (at_error.empty())
{ {
@@ -1646,7 +1664,7 @@ TEST_CASE("json_view JSON pointers")
} }
else else
{ {
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == without_path(exception_of([&] { static_cast<void>(j[p]); }))); CHECK(exception_of([&] { static_cast<void>(v[p]); }) == exception_of([&] { static_cast<void>(j[p]); }));
} }
// (basic_json::contains() throws out_of_range.404 for an empty // (basic_json::contains() throws out_of_range.404 for an empty
// array index token, although it is not meant to throw; the view // array index token, although it is not meant to throw; the view
@@ -1657,7 +1675,7 @@ TEST_CASE("json_view JSON pointers")
static_cast<void>(found); static_cast<void>(found);
}); });
CHECK(v.contains(p) == (contains_error.empty() && j.contains(p))); CHECK(v.contains(p) == (contains_error.empty() && j.contains(p)));
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast<void>(j.value(p, 5)); }))); CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == exception_of([&] { static_cast<void>(j.value(p, 5)); }));
} }
} }
#endif #endif
+247 -1
View File
@@ -13,6 +13,7 @@
#include <nlohmann/detail/view/builder.hpp> #include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/materialize.hpp> #include <nlohmann/detail/view/materialize.hpp>
#include <nlohmann/detail/view/string_ref.hpp> #include <nlohmann/detail/view/string_ref.hpp>
#include <nlohmann/json_view.hpp> // json_document::accept
#else #else
#include <nlohmann/json_view.hpp> // the single header contains the internal headers #include <nlohmann/json_view.hpp> // the single header contains the internal headers
#endif #endif
@@ -59,7 +60,8 @@ built build(const std::string& text, bool comments, bool trailing_commas, bool s
r.copy.assign(text.begin(), text.end()); r.copy.assign(text.begin(), text.end());
src = r.copy.data(); src = r.copy.data();
} }
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure); constexpr bool nul_is_end = !nlohmann::detail::abi_config::strict_nul_handling;
r.ok = nlohmann::detail::view::build<FloatType, nul_is_end>(*r.data, src, text.size(), comments, trailing_commas, sentinel, false, r.failure);
r.data->src = src; r.data->src = src;
r.data->base[0] = src; r.data->base[0] = src;
r.data->base[1] = r.data->arena.data(); r.data->base[1] = r.data->arena.data();
@@ -172,6 +174,22 @@ void check_string(const std::string& content)
} }
} }
// accept() stores no nodes: it must accept and reject what json::accept does, for all options and for inputs with
// and without a terminating NUL
void check_accept(const std::string& text)
{
CAPTURE(text)
const std::vector<char> bytes(text.begin(), text.end());
for (int options = 0; options < 4; ++options)
{
const bool comments = (options & 1) != 0;
const bool trailing_commas = (options & 2) != 0;
const bool accepted = json::accept(text, comments, trailing_commas);
CHECK(nlohmann::json_document::accept(text, comments, trailing_commas) == accepted);
CHECK(nlohmann::json_document::accept(bytes, comments, trailing_commas) == accepted);
}
}
// a small deterministic generator of documents // a small deterministic generator of documents
struct generator struct generator
{ {
@@ -423,6 +441,133 @@ TEST_CASE("json_view builder")
} }
} }
SECTION("accept() without an index")
{
for (const char* text :
{
"", " ", "\n", "null", "nul", "true", "[", "]", "[]", "{}", "[1,2,3]", "[1,2,3", "[1,2,3,]", "{\"a\":1,}", "{\"a\"}", "{\"a\":}",
"[1] x", "[1][2]", "\xEF\xBB\xBF[1]", "[\"a\\", R"(["\u12"])", R"(["\ud800"])", "[\"\x01\"]", "[\"\xff\"]", "[01]", "[1.]",
"[.5]", "[-]", "[1e]", "// c\n[1]", "/* c */ {\"a\" /* x */ : 1}", "/* c", "[1] // c", "1e400", "-1e400", "123456789012345678901234567890",
R"({"a":1,"a":2})", "[[[]]]", R"("\u00e4\n\ud83d\ude00")"
})
{
check_accept(text);
}
// a null byte inside the text, and after it
check_accept(std::string("[1,\0]", 5));
check_accept(std::string("[1]\0", 4));
check_accept(std::string("\0", 1));
// generated documents and damaged copies, as for the builder above
generator g;
for (int i = 0; i < 2000; ++i)
{
std::string text;
g.value(text, 0);
check_accept(text);
std::string damaged = text;
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
damaged[at] = replacements[g.r(16)];
check_accept(damaged);
check_accept(text.substr(0, at));
}
// nesting beyond the 64 levels that are kept on the builder's own stack, closed and not closed
for (const std::size_t depth :
{
63u, 64u, 65u, 66u, 200u, 1000u
})
{
check_accept(std::string(depth, '[') + std::string(depth, ']'));
check_accept(std::string(depth, '[') + std::string(depth - 1, ']'));
std::string objects;
for (std::size_t i = 0; i < depth; ++i)
{
objects += "{\"k\":";
}
objects += "1" + std::string(depth, '}');
check_accept(objects);
check_accept(objects.substr(0, objects.size() - 1));
}
// objects with enough members to get a hash index when they are indexed (and duplicate keys)
std::string large = "{";
for (int i = 0; i < 300; ++i)
{
large += (i == 0 ? "\"k" : ",\"k") + std::to_string(i % 200) + "\":" + std::to_string(i);
}
check_accept(large + "}");
check_accept(large);
check_accept("[" + large + "},{" + large.substr(1) + "}]");
// the overflow check uses the floating-point type of the document
using float_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
for (const char* text :
{
"1e39", "-1e39", "3.4028235e38", "3.4028236e38", "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})", "340282356779733661637539395458142568448"
})
{
CAPTURE(text)
CHECK(nlohmann::basic_json_document<float_json>::accept(text) == float_json::accept(text));
}
// strings are checked and not decoded: runs of text of every length from 0 to 40 between valid and invalid
// escapes, UTF-8 sequences and control characters, in values and in keys, cut near the end of the text
const std::array<const char*, 9> valid = {{"\\n", "\\\"", "\\\\", "\\/", "\\t", "\\u00e4", "\\u20ac", "\\ud83d\\ude00", "\\u0000"}};
const std::array<const char*, 12> invalid = {{"\\x", "\\ud800", "\\udc00", "\\u12G4", "\\u12", "\\ud83dx", "\\ud83d\\u0041", "\\ud83d\\ud83d", "\\", "\x01", "\x1f", "\x7f"}};
const std::array<const char*, 6> utf8 = {{"\xc3\xa4", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xc3", "\xff", "\x80"}};
std::mt19937 rng(2029); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (std::size_t run = 0; run <= 40; ++run)
{
for (int i = 0; i < 30; ++i)
{
std::string content = std::string(rng() % 20, 'a');
const auto pieces = 1 + (rng() % 6);
for (unsigned k = 0; k < pieces; ++k)
{
content += valid[rng() % valid.size()];
content += std::string(k == 0 ? run : rng() % 41, static_cast<char>('b' + (rng() % 20)));
switch (rng() % 8)
{
case 0:
content += utf8[rng() % 3];
break;
case 1:
content += utf8[rng() % utf8.size()];
break;
case 2:
content += invalid[rng() % invalid.size()];
break;
default:
break;
}
}
for (const std::string& text :
{
"[\"" + content + "\"]", "{\"" + content + "\":1}", "[\"" + content, "[\"" + content.substr(0, content.size() - (rng() % 4)) + "\"]"
})
{
check_accept(text);
}
}
}
}
SECTION("escapes in a row")
{
for (const char* content :
{
"\\n", R"(\n\t\\)", R"(\u00e9\u00e9\u4e2d)", "\\u00e9\\u00e", "\\u00e9\\", "\\n\\q", R"(\n\ud83d\ude00\n)",
R"(\ud83d\ude00\ud83d)", R"(\n\n\n abc \n\n)", "\\n\x01", "\\n\xC3\x28", "\\n\\"
})
{
check_accept(std::string("[\"") + content + "\"]");
check_accept(std::string("[\"") + content);
check_accept(std::string("{\"") + content + "\":1}");
}
}
SECTION("test files") SECTION("test files")
{ {
for (const char* name : for (const char* name :
@@ -523,6 +668,54 @@ TEST_CASE("json_view builder: strings across vector blocks")
} }
} }
TEST_CASE("json_view builder: fraction digits")
{
// The digits of a fraction are skipped 16 bytes at a time where there are
// that many left (SSE2), and one at a time after that. Fractions of every
// length from 0 to 40 digits, followed by every kind of byte that ends a
// number or lies next to the digits in ASCII ('/' and ':'), as values, as
// keys' neighbours, and at the very end of the text (a vector without a
// terminating NUL). The length of the number is part of the value that
// the test compares with json::parse().
const std::string followers = ",]}/:.eE+-x \n\t\"09";
std::mt19937 rng(2028); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (std::size_t digits = 0; digits <= 40; ++digits)
{
for (int i = 0; i < 3; ++i)
{
std::string fraction;
for (std::size_t k = 0; k < digits; ++k)
{
fraction += static_cast<char>('0' + (rng() % 10));
}
const std::string number = std::string(rng() % 2 == 0 ? "-" : "") + std::to_string(rng() % 100000) + "." + fraction;
check_same(number);
check_same("[" + number + "]");
std::string pair;
pair.reserve(number.size() * 2 + 3);
pair += '[';
pair += number;
pair += ',';
pair += number;
pair += ']';
check_same(pair);
check_same("{\"a\":" + number + "}");
for (const char follower : followers)
{
check_same(number + follower);
check_same("[" + number + follower + "]");
check_same("[" + number + follower + std::string(rng() % 20, '1'));
}
// everything near the end of the text: the number cut in every place
for (std::size_t cut = 1; cut <= 18 && cut <= number.size(); ++cut)
{
check_same(number.substr(0, number.size() - cut));
check_same("[" + number.substr(0, number.size() - cut));
}
}
}
}
TEST_CASE("json_view builder: escapes between runs of text") TEST_CASE("json_view builder: escapes between runs of text")
{ {
// The text between escapes is copied 16 bytes at a time where there are // The text between escapes is copied 16 bytes at a time where there are
@@ -575,6 +768,59 @@ TEST_CASE("json_view builder: escapes between runs of text")
} }
} }
TEST_CASE("json_view builder: chains of unicode escapes")
{
// Consecutive \uXXXX escapes are decoded in a loop of their own. Chains of
// every length from 1 to 40 of escapes that give one, two, and three
// bytes of UTF-8, in upper and lower case, followed by what ends a chain:
// the end of the string, text, another escape, UTF-8, and everything that
// is not a valid escape (a bad digit at every position of the last
// escape, a lone surrogate, a surrogate pair, a cut in every place).
const std::array<const char*, 8> chain = {{"\\u0041", "\\u00e4", "\\u00E4", "\\u20ac", "\\u3042", "\\uFFFF", "\\u0000", "\\u07ff"}};
const std::array<const char*, 12> ends = {{"", "abc", "\\n", "\\u00e9", "\\ud83d\\ude00", "\\ud83d", "\\ude00", "\\ud83d\\u0041", "\\u12G4", "\\u12", "\xc3\xa4", "\\"}};
std::mt19937 rng(2027); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
for (std::size_t length = 1; length <= 40; ++length)
{
for (int i = 0; i < 20; ++i)
{
std::string content = std::string(rng() % 20, 'a');
for (std::size_t k = 0; k < length; ++k)
{
content += chain[rng() % chain.size()];
}
const std::string whole = content;
for (const char* end : ends)
{
check_string(whole + end);
check_string(whole + end + std::string(rng() % 19, 'z'));
}
// a bad digit at every position of every escape near the end of the chain
for (std::size_t back = 1; back <= 6 && back <= length; ++back)
{
for (std::size_t digit = 2; digit < 6; ++digit)
{
std::string broken = whole;
broken[broken.size() - (6 * back) + digit] = "gG\"\x01 z"[rng() % 6];
check_string(broken);
}
}
// the text cut in every place of the last three escapes
for (std::size_t cut = 0; cut <= 18 && cut <= whole.size(); ++cut)
{
check_string(whole.substr(0, whole.size() - cut));
}
}
}
// a chain longer than the first growth of the arena, in a key and in a value
std::string long_chain;
for (int i = 0; i < 3000; ++i)
{
long_chain += chain[static_cast<std::size_t>(i) % chain.size()];
}
check_string(long_chain);
check_string(long_chain + "\\ud83d\\ude00" + long_chain);
}
TEST_CASE("json_view node integer bits") TEST_CASE("json_view node integer bits")
{ {
using nlohmann::detail::view::integer_bits; using nlohmann::detail::view::integer_bits;
@@ -0,0 +1,83 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::json_document;
#include <functional>
#include <map>
#include <string>
// These tests were split off unit-json_view_compare.cpp, whose object file got
// too large for the MinGW assembler ("file too big").
namespace
{
// an object type that does not enumerate its members in ascending order of keys
template<typename Key, typename Value, typename /*Compare*/, typename Allocator>
using descending_object = std::map<Key, Value, std::greater<Key>, Allocator>;
} // namespace
TEST_CASE("json_view comparison of differing values")
{
SECTION("values that differ in their first bytes, or are written differently")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
const bool equal = dx.root() == dy.root();
CHECK(equal == (json::parse(x) == json::parse(y)));
CHECK(equal == (dx.root() == json::parse(y)));
const nlohmann::json_editable_document ex = nlohmann::json_editable_document::parse(x);
CHECK(equal == (ex.root() == dy.root()));
return equal;
};
// booleans
CHECK(!same("true", "false"));
CHECK(!same("[true]", "[false]"));
CHECK(!same(R"({"a":false,"b":[1]})", R"({"a":true,"b":[1]})"));
CHECK(same(R"({"a":true,"b":[1]})", R"({"a":true,"b":[1]})"));
// strings and keys of equal length that differ in the first eight bytes (they are compared in words)
CHECK(!same(R"("abcdefghijklmnopqrstuvwxyz")", R"("Xbcdefghijklmnopqrstuvwxyz")"));
CHECK(!same(R"(["abcdefghijklmnopq",1])", R"(["abcdefghXjklmnopq",1])"));
CHECK(!same(R"({"abcdefghijklmnopq":1})", R"({"Xbcdefghijklmnopq":1})"));
CHECK(!same(R"({"abcdefghijklmnopq":[1]})", R"({"abcdefghijklmnopX":[1]})"));
// floats of one layout and different tokens are the same value
CHECK(same("[1.5e0, 2]", "[1.5E0, 2]"));
CHECK(same("[1.5e0, 2]", "[1.5e+0, 2]"));
CHECK(same("[1.50, 2]", "[1.5, 2]"));
CHECK(!same("[1.5e0, 2]", "[1.6e0, 2]"));
CHECK(!same("[1.5e0, 2]", "[1.5E1, 2]"));
CHECK(same(R"({"a":1.25e0,"b":[3]})", R"({"a":1.25E0,"b":[3]})"));
}
SECTION("a basic_json whose members are not sorted")
{
// (an object type that is not an ordered map, and does not keep its members sorted either)
using descending_json = nlohmann::basic_json<descending_object>;
using descending_document = nlohmann::basic_json_document<descending_json>;
std::string text = "{";
for (char key = 'a'; key <= 'p'; ++key)
{
text += (key != 'a' ? ",\"" : "\"") + std::string(1, key) + "\":[" + std::to_string(key) + "]";
}
text += "}";
const descending_json j = descending_json::parse(text);
const descending_document d = descending_document::parse(text);
CHECK(d.root() == j);
CHECK(j == d.root());
descending_json other = j;
other["k"] = 0;
CHECK(d.root() != other);
CHECK(other != d.root());
}
}
+112 -672
View File
@@ -35,9 +35,9 @@ using json_view_test::has_duplicate_keys;
#include <string> #include <string>
#include <vector> #include <vector>
// These tests were split off unit-json_view.cpp, whose object file got too large // These tests were split off unit-json_view.cpp, whose object file got too large for the MinGW
// for the MinGW linker (see json_view_test_helpers.hpp). Like that file, this one // linker (see json_view_test_helpers.hpp). Like that file, this one is also built
// is also built with C++17, as it mentions JSON_HAS_CPP_17. // with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view dump") TEST_CASE("json_view dump")
{ {
@@ -101,6 +101,10 @@ TEST_CASE("json_view dump")
CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]"); CHECK(d.root().dump(0, ' ', false, json_view::number_format::source) == "[\n1.50,\n1E2,\n-0,\n-0.0,\n123456789012345678901234567890,\n18446744073709551615,\n-9223372036854775808,\n0.1,\n1e-7,\n5e-324\n]");
CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]"); CHECK(d.root().dump(-1, ' ', true, json_view::number_format::source) == "[1.50,1E2,-0,-0.0,123456789012345678901234567890,18446744073709551615,-9223372036854775808,0.1,1e-7,5e-324]");
CHECK(d.root().dump(0, ' ', true, json_view::number_format::source) == d.root().dump(0, ' ', false, json_view::number_format::source));
const json_document escaped = json_document::parse("[\"\xC3\xA9\", 1.50, {\"\xF0\x9F\x98\x80\": 1E2}]");
CHECK(escaped.root().dump(1, ' ', true, json_view::number_format::source) == "[\n \"\\u00e9\",\n 1.50,\n {\n \"\\ud83d\\ude00\": 1E2\n }\n]");
// float tokens of up to 17 significant digits in every spelling: those // float tokens of up to 17 significant digits in every spelling: those
// of at most 15 digits are written from their digits, the others // of at most 15 digits are written from their digits, the others
// through the conversion; both as dump() writes them // through the conversion; both as dump() writes them
@@ -150,6 +154,111 @@ TEST_CASE("json_view dump")
CHECK(many_doc.root().dump() == json::parse(many_tokens).dump()); CHECK(many_doc.root().dump() == json::parse(many_tokens).dump());
} }
// tokens of 16 and 17 digits without an exponent (like GeoJSON
// coordinates), which dump() converts in batches: doubles exactly
// halfway between two candidates for the shortest digits, random
// digits, and digits near a shorter decimal (...99999997,
// ...00000003), whose rounding carries; in pairs and among other
// values, so that the batches are interrupted; the last token near the
// end of the text
{
std::mt19937_64 tokens(5627); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
const auto draw = [&tokens](std::size_t n)
{
const std::uint64_t r = tokens() % n;
return static_cast<std::size_t>(r);
};
std::string coordinates = "[754570942.33984373,257769445.56835938,858106901.94921875,97944964.876953125,"
"[0.5,1e-7],[1.5,-0.0,2.5],[1E2,12.5,0.1,3.25,4.5,5.75,6.125,7.0,8.5],[],[1.5],[1,2.5],[2.5,\"x\"],[[1.5],2.5],";
for (int i = 0; i < 20000; ++i)
{
const std::size_t length = 16 + draw(2);
std::string digits(1, static_cast<char>('1' + draw(9)));
const std::size_t kept = draw(3) == 0 ? length : 2 + draw(length - 3);
for (std::size_t k = 1; k < kept; ++k)
{
digits += static_cast<char>('0' + draw(10));
}
digits += std::string(length - kept, draw(2) == 0 ? '9' : '0');
digits.back() = static_cast<char>('0' + draw(10));
const std::size_t point = 1 + draw(9);
const std::string token = (draw(2) == 0 ? "-" : "") + digits.substr(0, point) + "." + digits.substr(point);
if (i != 0)
{
coordinates += ',';
}
if (draw(8) == 0)
{
coordinates += R"({"x":)";
coordinates += token;
coordinates += R"(,"s":"a"})";
}
else
{
coordinates += '[';
coordinates += token;
coordinates += ',';
coordinates += token;
coordinates += ']';
}
}
coordinates += ",-65.613616999999977]";
const json_document parsed = json_document::parse(coordinates);
const ordered_json j = ordered_json::parse(coordinates);
CHECK(parsed.root().dump() == j.dump());
CHECK(parsed.root().dump(1) == j.dump(1));
CHECK(parsed.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(parsed.root().back().get<double>() == j.back().get<double>());
CHECK(parsed.root().back().dump() == "-65.61361699999998");
std::size_t i = 0;
for (const json_view v : parsed.root())
{
const ordered_json& e = j[i++];
CHECK(v.dump() == e.dump());
if (v.is_array() && !v.empty() && v[0].is_number())
{
CHECK(v[0].get<double>() == e[0].get<double>());
}
}
// an editable document: edits move some arrays, so that the walk
// leaves the order of the parsed nodes
ordered_json_editable_document editable = ordered_json_editable_document::parse(coordinates);
ordered_json expected = j;
for (std::size_t k = 0; k < 20000; k += 97)
{
if (expected[k].is_array())
{
editable.push_back(editable.root()[k], ordered_json(1.5));
expected[k].push_back(1.5);
}
}
CHECK(editable.root().dump() == expected.dump());
}
// tokens of 16 and 17 digits with few digits after the point, and with a value of 0, which are
// converted by the general algorithm; padded, as a token near the end of the text is not read in this way
{
const std::string padding(40, ' ');
for (const char* token :
{
"123456789012.3456", "-123456789012.3456", "1234567890123.456", "12345678901234.56", "123456789012345.6", "1234567890123456.0",
"0.0000000000000000", "-0.0000000000000000", "0.000000000000000", "1234567890123.4567", "99999999999999.999"
})
{
CAPTURE(token)
const std::string padded = std::string("[") + token + padding + "]";
const json_document padded_document = json_document::parse(padded);
const json padded_json = json::parse(padded);
CHECK(padded_document.root()[0].get<double>() == padded_json[0].get<double>());
CHECK(padded_document.root().dump() == padded_json.dump());
// a float document converts the tokens as well
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
const nlohmann::basic_json_document<json_float> float_doc = nlohmann::basic_json_document<json_float>::parse(padded);
CHECK(float_doc.root()[0].get<float>() == json_float::parse(padded)[0].get<float>());
}
}
// random doubles, written as parse() and dump() would // random doubles, written as parse() and dump() would
std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) std::mt19937_64 rng(1170); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::string many = "["; std::string many = "[";
@@ -171,673 +280,4 @@ TEST_CASE("json_view dump")
const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]"); const nlohmann::basic_json_document<json_float> float_document = nlohmann::basic_json_document<json_float>::parse("[0.1, 1.5e10, 3.4028235e38]");
CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump()); CHECK(float_document.root().dump() == json_float::parse("[0.1, 1.5e10, 3.4028235e38]").dump());
} }
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
{
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
for (const char* const special :
{
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
})
{
for (std::size_t length = 0; length <= 40; ++length)
{
for (std::size_t position = 0; position <= length; ++position)
{
std::string content(length, 'a');
content.insert(position, special);
const std::array<std::string, 3> texts =
{
{
"\"" + content + "\"",
"[\"" + content + "\"]",
"{\"" + content + "\":1}",
}
};
for (const std::string& text : texts)
{
CAPTURE(text)
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
}
}
}
}
}
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
{
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
std::string text = "[";
std::size_t in_chunk = 0;
const auto append = [&](std::uint32_t cp)
{
if (cp < 0x80)
{
if (cp < 0x20 || cp == '"' || cp == '\\')
{
return;
}
text += static_cast<char>(cp);
}
else if (cp < 0x800)
{
text += static_cast<char>(0xC0 | (cp >> 6));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else if (cp < 0x10000)
{
text += static_cast<char>(0xE0 | (cp >> 12));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else
{
text += static_cast<char>(0xF0 | (cp >> 18));
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
};
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
{
if (cp >= 0xD800 && cp < 0xE000)
{
continue;
}
if (in_chunk == 0)
{
text += text.size() > 1 ? ",\"" : "\"";
}
append(cp);
if (++in_chunk == 37)
{
text += "\"";
in_chunk = 0;
}
}
text += in_chunk != 0 ? "\"]" : "]";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(2) == j.dump(2));
}
SECTION("deep nesting, indented")
{
// indentations of up to and beyond the width of the writer's fixed-size copy
for (const int depth :
{
1, 5, 20, 70, 300
})
{
std::string text;
for (int i = 0; i < depth; ++i)
{
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
}
text += "[]";
for (int i = depth - 1; i >= 0; --i)
{
text += i % 2 == 0 ? "]" : "}";
}
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
0, 1, 2, 3, 8
})
{
CHECK(d.root().dump(indent) == j.dump(indent));
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
}
}
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
{
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
// the second text has a DEL, which ensure_ascii writes as an escape
for (const char* const del :
{"", "\x7F"
})
{
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
+ "\"n\":[1,2.5,-0,true,null]}";
const ordered_json j = ordered_json::parse(text);
const ordered_json_document d = ordered_json_document::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
// strings that an edit wrote (the source strings that they replace were plain)
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
ordered_json k = j;
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
k["plain"] = "\xC3\xA4gypten";
e.set(e.root()["list"][0], std::string("ascii after edit"));
k["list"][0] = "ascii after edit";
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
k["list"][1] = "\xC3\xA4";
e.set(e.root()["del"], std::string("tab\there"));
k["del"] = "tab\there";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
CHECK(e.root().dump() == k.dump());
// strings that were edited from or to ASCII text
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
k["list"][2] = "\xC3\xA4";
e.set(e.root()["list"][4], std::string("plain again"));
k["list"][4] = "plain again";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
}
}
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
{
// the check whether a string of the source has no byte >= 0x7F reads
// eight bytes at a time and then the rest one by one
for (std::size_t length = 1; length <= 20; ++length)
{
for (std::size_t pos = 0; pos < length; ++pos)
{
for (const char* const c :
{
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
})
{
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
std::string text = "[\"";
text.append(s).append("\",{\"").append(s).append("\":1}]");
const json_document d = json_document::parse(text);
const json j = json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
}
}
}
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
std::map<std::string, std::size_t> occurrences;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
++occurrences[m.key];
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == occurrences[entry.first]);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
}
SECTION("keys that differ in their last bytes are spread over the table")
{
// The hash must spread the usual families of keys (numbered names, hex
// strings, dates, short names): a key farther than index_max_displacement
// from its home slot would leave the object without a table. The keys
// are placed with linear probing, as the table does.
const auto max_displacement = [](const std::vector<std::string>& keys)
{
std::size_t slots = 16;
while (slots < 2 * keys.size())
{
slots *= 2;
}
std::vector<bool> used(slots, false);
std::size_t farthest = 0;
for (const std::string& key : keys)
{
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
std::size_t distance = 0;
while (used[i])
{
i = (i + 1) & (slots - 1);
++distance;
}
used[i] = true;
farthest = (std::max)(farthest, distance);
}
return farthest;
};
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
// (a format string that is not a literal is a Flawfinder error)
const auto numbered = [](const char* format, int count, int step)
{
const std::string pattern(format);
const std::size_t percent = pattern.find('%');
std::size_t conversion = percent + 1;
std::size_t width = 0;
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
{
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
++conversion;
}
const bool hex = pattern[conversion] == 'x';
std::vector<std::string> keys;
for (int i = 0; i < count; ++i)
{
std::ostringstream key;
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
keys.push_back(key.str());
}
return keys;
};
for (const char* format :
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
})
{
CAPTURE(format)
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
}
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
for (const std::size_t length :
{
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
})
{
CAPTURE(length)
std::set<std::string> distinct;
// (there are 26 and 676 keys of one and two letters)
std::size_t wanted = 700;
if (length == 1)
{
wanted = 26;
}
else if (length == 2)
{
wanted = 500;
}
while (distinct.size() < wanted)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
distinct.insert(key);
}
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
}
}
SECTION("keys of every length that differ in one byte")
{
// In a large object, a key is found by comparing its bytes with those of the member the
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::vector<std::string> keys;
std::set<std::string> distinct;
for (std::size_t length = 0; length <= 40; ++length)
{
for (int copy = 0; copy < 5; ++copy)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
if (distinct.insert(key).second)
{
keys.push_back(key);
}
}
}
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
}
text += "}";
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
const json_document d = json_document::parse(text);
const json_view v = d.root();
for (std::size_t i = 0; i < keys.size(); ++i)
{
CAPTURE(keys[i])
CHECK(v[keys[i]].get<std::size_t>() == i);
CHECK(v.at(keys[i]).get<std::size_t>() == i);
CHECK(v.contains(keys[i]));
for (std::size_t at = 0; at < keys[i].size(); ++at)
{
std::string other = keys[i];
other[at] = other[at] == 'z' ? 'y' : 'z';
CAPTURE(other)
CHECK(v.contains(other) == (distinct.count(other) != 0));
}
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
if (!keys[i].empty())
{
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
}
}
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
} }
+338
View File
@@ -0,0 +1,338 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <map>
#include <random>
#include <set>
#include <sstream>
#include <string>
#include <vector>
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
// with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view dump: escaping, nesting, and output buffers")
{
SECTION("members in document order, all of them")
{
const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})");
CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})");
CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}");
// dump() writes all members, though the lookup finds the first
CHECK(d.root()["b"].dump() == "1");
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document deep_document = json_document::parse(deep);
CHECK(deep_document.root().dump() == deep);
}
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
{
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
for (const char* const special :
{
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
})
{
for (std::size_t length = 0; length <= 40; ++length)
{
for (std::size_t position = 0; position <= length; ++position)
{
std::string content(length, 'a');
content.insert(position, special);
const std::array<std::string, 3> texts =
{
{
"\"" + content + "\"",
"[\"" + content + "\"]",
"{\"" + content + "\":1}",
}
};
for (const std::string& text : texts)
{
CAPTURE(text)
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
}
}
}
}
}
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
{
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
std::string text = "[";
std::size_t in_chunk = 0;
const auto append = [&](std::uint32_t cp)
{
if (cp < 0x80)
{
if (cp < 0x20 || cp == '"' || cp == '\\')
{
return;
}
text += static_cast<char>(cp);
}
else if (cp < 0x800)
{
text += static_cast<char>(0xC0 | (cp >> 6));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else if (cp < 0x10000)
{
text += static_cast<char>(0xE0 | (cp >> 12));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
else
{
text += static_cast<char>(0xF0 | (cp >> 18));
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
text += static_cast<char>(0x80 | (cp & 0x3F));
}
};
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
{
if (cp >= 0xD800 && cp < 0xE000)
{
continue;
}
if (in_chunk == 0)
{
text += text.size() > 1 ? ",\"" : "\"";
}
append(cp);
if (++in_chunk == 37)
{
text += "\"";
in_chunk = 0;
}
}
text += in_chunk != 0 ? "\"]" : "]";
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root().dump(2) == j.dump(2));
}
SECTION("deep nesting, indented")
{
// indentations of up to and beyond the width of the writer's fixed-size copy
for (const int depth :
{
1, 5, 20, 70, 300
})
{
std::string text;
for (int i = 0; i < depth; ++i)
{
text += i % 2 == 0 ? "[1," : R"({"k":2,"\u00e9":)";
}
text += "[]";
for (int i = depth - 1; i >= 0; --i)
{
text += i % 2 == 0 ? "]" : "}";
}
const ordered_json_document d = ordered_json_document::parse(text);
const ordered_json j = ordered_json::parse(text);
for (const int indent :
{
0, 1, 2, 3, 8
})
{
CHECK(d.root().dump(indent) == j.dump(indent));
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
}
}
}
SECTION("the output buffer of a small value is small")
{
// an escaped key after the value: its node lies in the arena, so the
// source extent of the value cannot be read from the next node
const std::string big(100000, 'a');
const std::string text = R"({"small":1,"list":[1,2,3],"k\n":")" + big + R"("})";
const json_document d = json_document::parse(text);
const auto small = d.root()["small"].dump();
CHECK(small == "1");
CHECK(small.capacity() < 4096);
const auto list = d.root()["list"].dump();
CHECK(list == "[1,2,3]");
CHECK(list.capacity() < 4096);
CHECK(d.root()["list"].dump(2).capacity() < 4096);
// the whole document and the large value are unaffected
CHECK(d.root().dump() == ordered_json::parse(text).dump());
CHECK(d.root()["k\n"].dump() == "\"" + big + "\"");
}
SECTION("output that outgrows the estimate")
{
// ensure_ascii writes six bytes for each two-byte character
std::string chars;
for (int i = 0; i < 5000; ++i)
{
chars += "\xC3\xA9";
}
const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})");
const json expected = json::parse("\"" + chars + "\"");
CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true));
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
}
SECTION("the estimate for a value that is followed by many decoded strings")
{
// (the end of the value in the source is not known when the nodes after it have decoded strings)
std::string text = "[7";
for (int i = 0; i < 20; ++i)
{
text += R"(,"\n")";
}
text += ",8]";
const json_document d = json_document::parse(text);
CHECK(d.root()[0].dump() == "7");
CHECK(d.root()[1].dump() == R"("\n")");
CHECK(d.root().dump() == json::parse(text).dump());
// the same with a container
const json_document nested = json_document::parse(R"([[7],)" + text.substr(3));
CHECK(nested.root()[0].dump() == "[7]");
}
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
{
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
// the second text has a DEL, which ensure_ascii writes as an escape
for (const char* const del :
{"", "\x7F"
})
{
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
+ "\"n\":[1,2.5,-0,true,null]}";
const ordered_json j = ordered_json::parse(text);
const ordered_json_document d = ordered_json_document::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
CHECK(d.root().dump() == j.dump());
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
// strings that an edit wrote (the source strings that they replace were plain)
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
ordered_json k = j;
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
k["plain"] = "\xC3\xA4gypten";
e.set(e.root()["list"][0], std::string("ascii after edit"));
k["list"][0] = "ascii after edit";
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
k["list"][1] = "\xC3\xA4";
e.set(e.root()["del"], std::string("tab\there"));
k["del"] = "tab\there";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
CHECK(e.root().dump() == k.dump());
// strings that were edited from or to ASCII text
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
k["list"][2] = "\xC3\xA4";
e.set(e.root()["list"][4], std::string("plain again"));
k["list"][4] = "plain again";
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
}
}
SECTION("the length of the run that ensure_ascii copies")
{
// (dump() only asks for strings with a byte to escape, so a run that ends the string is checked here)
for (const std::size_t length :
{
std::size_t{0}, std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{31}, std::size_t{32}, std::size_t{48}, std::size_t{50}
})
{
CAPTURE(length)
// a run of printable characters, followed by a byte that is not copied and more text
std::string text(length, 'a');
text += "\"abcdefghijklmnopqrstuvwxyz";
const auto* const bytes = reinterpret_cast<const unsigned char*>(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const auto* const end = bytes + text.size();
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, end) == length);
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 1, end) == length);
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length + 5, end) == length);
// the end of the buffer right after the run
CHECK(nlohmann::detail::view::ascii_copyable_run(bytes, length, bytes + length) == length);
}
}
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
{
// the check whether a string of the source has no byte >= 0x7F reads
// eight bytes at a time and then the rest one by one
for (std::size_t length = 1; length <= 20; ++length)
{
for (std::size_t pos = 0; pos < length; ++pos)
{
for (const char* const c :
{
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
})
{
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
std::string text = "[\"";
text.append(s).append("\",{\"").append(s).append("\":1}]");
const json_document d = json_document::parse(text);
const json j = json::parse(text);
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
}
}
}
}
SECTION("streams and discarded views")
{
const json_document d = json_document::parse(R"({"a": [1, 2]})");
std::ostringstream compact;
compact << d.root();
CHECK(compact.str() == R"({"a":[1,2]})");
std::ostringstream pretty;
pretty << std::setw(2) << std::setfill('.') << d.root() << d.root()["a"];
CHECK(pretty.str() == "{\n..\"a\": [\n....1,\n....2\n..]\n}[1,2]");
CHECK(json_view().dump() == json(json::value_t::discarded).dump());
}
}
+28 -11
View File
@@ -532,21 +532,25 @@ TEST_CASE("json_view edits: errors")
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), json_view_test::message("type_error.305", "/a", 21, 27, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), json_view_test::message("type_error.305", "/o", 6, 14, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index -1 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&); CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), json_view_test::message("type_error.308", "/o", 6, 14, "cannot use push_back() with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&); CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), json_view_test::message("type_error.309", "/n", 34, 35, "cannot use insert() with number").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&); CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), json_view_test::message("type_error.307", "/n", 34, 35, "cannot use erase() with number").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&); CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), json_view_test::message("type_error.307", "/o", 6, 14, "cannot use erase() with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), json_view_test::message("out_of_range.403", "", 0, 47, "key 'missing' not found").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json(json::value_t::discarded)), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
// (an index that does not fit the document, in a null: basic_json would pad it with 4 Gi nulls)
CHECK_THROWS_AS(d.set(json::json_pointer("/z/4294967295"), 1), json::out_of_range&);
CHECK(root["z"].is_null());
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
// invalid UTF-8 is rejected when it enters the document, with the error // invalid UTF-8 is rejected when it enters the document, with the error
@@ -572,6 +576,19 @@ TEST_CASE("json_view edits: errors")
static_cast<void>(other); static_cast<void>(other);
} }
TEST_CASE("json_view edits: a container that was added is replaced by a scalar")
{
// the view of an element that push_back() added is that of the new value itself, not of
// the slot that links to it: replacing it by a scalar has to find the container it is in
json_editable_document d = json_editable_document::parse(R"({"a": [1]})");
d.push_back(d.root()["a"], json::array({2, 3}));
d.push_back(d.root()["a"], json::object({{"k", json::array({4, 5})}}));
d.set(d.root()["a"][1], 6);
d.set(d.root()["a"][2]["k"], "s");
CHECK(d.root().dump() == R"({"a":[1,6,{"k":"s"}]})");
CHECK(d.root().materialize() == json::parse(R"({"a":[1,6,{"k":"s"}]})"));
}
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: the size of a text arena") TEST_CASE("json_view edits: the size of a text arena")
{ {
+105 -7
View File
@@ -39,6 +39,7 @@ using nlohmann::ordered_json_document;
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
#include <optional> #include <optional>
#include <string_view>
#endif #endif
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose // NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
@@ -276,6 +277,17 @@ TEST_CASE("json_view from_json")
CHECK(true); CHECK(true);
} }
SECTION("a map with enum keys from a view that is not an object")
{
// an object is read with the keys as the enum's names; any other
// value is read as an array of pairs, like a map of any other key type
const json_document d = json_document::parse(R"({"pairs": [["red", 1], ["green", 2]], "number": 1})");
const json j = d.root().materialize();
const json_view v = d.root();
CHECK(same<std::map<color, int>>(v, j, "pairs"));
CHECK((v["pairs"].get<std::map<color, int>>() == std::map<color, int> {{color::red, 1}, {color::green, 2}}));
}
SECTION("std types: the same values as from the materialized value") SECTION("std types: the same values as from the materialized value")
{ {
const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]], const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]],
@@ -337,8 +349,7 @@ TEST_CASE("json_view from_json")
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3); CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
} }
// (the exceptions of views have neither the path nor the byte range of the value) #if !defined(JSON_NOEXCEPTION)
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
SECTION("std types: the same errors as from the materialized value") SECTION("std types: the same errors as from the materialized value")
{ {
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])"); const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
@@ -403,10 +414,10 @@ TEST_CASE("json_view from_json")
CHECK(d.root()[4].get<strict_color>() == strict_color::red); CHECK(d.root()[4].get<strict_color>() == strict_color::red);
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
const json j = d.root().materialize(); const json j = d.root().materialize();
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS #if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); })); CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
#endif #endif
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
#endif #endif
} }
@@ -443,6 +454,30 @@ TEST_CASE("json_view from_json")
CHECK(d.root().materialize().get<point>().x == 3); CHECK(d.root().materialize().get<point>().x == 3);
} }
SECTION("escaped keys: members are found by their decoded names")
{
// keys are decoded before they are compared, as by parse(); a lookup
// that compares the raw bytes of the source would miss these members
const json_document d = json_document::parse(R"([{"\u0078": 1, "y": 2}, {"V\u0061lue": 3, "Te\u0078t": "t"}, {"\u0062": "escaped"}, {"x": 1, "\u0078": 3, "y": 2}])");
const json j = d.root().materialize();
CHECK(d.root()[0].get<point>().x == 1);
CHECK(d.root()[0].get<point>().y == 2);
CHECK((d.root()[0].get<point>() == j[0].get<point>()));
CHECK(d.root()[1].get<with_names>().value == 3);
CHECK(d.root()[1].get<with_names>().text == "t");
CHECK(d.root()[2].get<with_defaults>().b == "escaped");
CHECK(d.root()[2].get<with_defaults>().a == 1);
// a duplicate key written once plain and once escaped: still the first member
CHECK(d.root()[3].get<point>().x == 1);
CHECK(j[3].get<point>().x == 3);
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
// an escaped backslash is part of the decoded key: "x\\" is not "x"
const json_document e = json_document::parse(R"({"x\\": 1, "y": 2})");
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == exception_of_call([&] { static_cast<void>(e.root().materialize().get<point>()); }));
CHECK(exception_of_call([&] { static_cast<void>(e.root().get<point>()); }) == "[json.exception.out_of_range.403] key 'x' not found");
#endif
}
SECTION("editable documents and ordered_json") SECTION("editable documents and ordered_json")
{ {
json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})"); json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})");
@@ -452,11 +487,9 @@ TEST_CASE("json_view from_json")
CHECK(o.root().get<shape>().center.y == 2); CHECK(o.root().get<shape>().center.y == 2);
} }
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS #if !defined(JSON_NOEXCEPTION)
SECTION("errors are those of the same conversion of a json value") SECTION("errors are those of the same conversion of a json value")
{ {
// (with JSON_DIAGNOSTICS, those of json name the path of the value,
// and with JSON_DIAGNOSTIC_POSITIONS its bytes)
const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])"); const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])");
const json j = d.root().materialize(); const json j = d.root().materialize();
for (std::size_t i = 0; i < 3; ++i) for (std::size_t i = 0; i < 3; ++i)
@@ -469,5 +502,70 @@ TEST_CASE("json_view from_json")
#endif #endif
} }
#ifdef JSON_HAS_CPP_17
namespace view_from_json
{
// a string_view member is not copied: it points into the source text or, for a
// string with escape sequences, into the buffer of decoded strings of the document
struct named_view
{
// initialized for GCC -Weffc++
// NOLINTNEXTLINE(readability-redundant-member-init)
std::string_view name{};
int n = 0;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(named_view, name, n)
};
} // namespace view_from_json
TEST_CASE("json_view from_json: string_view members borrow from the document")
{
using view_from_json::named_view;
// whether all of s is inside the bytes of text
const auto within = [](std::string_view s, std::string_view text)
{
const auto first = reinterpret_cast<std::uintptr_t>(text.data());
const auto begin = reinterpret_cast<std::uintptr_t>(s.data());
return begin >= first && begin + s.size() <= first + text.size();
};
SECTION("a string without escapes points into the source text")
{
const std::string text = R"({"name": "hello", "n": 1})";
const json_document d = json_document::parse(text);
REQUIRE_FALSE(d.owns_source());
const auto r = d.root().get<named_view>();
CHECK(r.name == "hello");
CHECK(r.n == 1);
CHECK(within(r.name, text));
CHECK(within(r.name, d.source()));
}
SECTION("a string with escapes points into the document, not into the source text")
{
const std::string text = R"({"name": "a\nb", "n": 2})";
json_document d = json_document::parse(text);
REQUIRE_FALSE(d.owns_source());
const auto r = d.root().get<named_view>();
CHECK(r.name == "a\nb");
CHECK(r.n == 2);
CHECK_FALSE(within(r.name, text));
// valid as long as the document is alive, even if the document is moved
const json_document moved = std::move(d);
CHECK(r.name == "a\nb");
CHECK(moved.root().get<named_view>().name == r.name);
}
SECTION("a document that owns its text keeps the members readable while it is alive")
{
const json_document d = json_document::parse(std::string(R"({"name": "owned", "n": 3})"));
REQUIRE(d.owns_source());
const auto r = d.root().get<named_view>();
CHECK(r.name == "owned");
CHECK(within(r.name, d.source()));
}
}
#endif
DOCTEST_CLANG_SUPPRESS_WARNING_POP DOCTEST_CLANG_SUPPRESS_WARNING_POP
DOCTEST_GCC_SUPPRESS_WARNING_POP DOCTEST_GCC_SUPPRESS_WARNING_POP
+7
View File
@@ -819,6 +819,13 @@ TEST_CASE("json_view images: errors")
static_cast<void>(d); static_cast<void>(d);
}) == prefix + "too short"); }) == prefix + "too short");
// an image that is moved into the document is checked as well
CHECK(exception_of([&]
{
const json_document d = json_document::load(std::vector<std::uint8_t>(10, 0));
static_cast<void>(d);
}) == prefix + "too short");
std::vector<std::uint8_t> bad = image; std::vector<std::uint8_t> bad = image;
bad[0] = 'X'; bad[0] = 'X';
CHECK(load_result(bad, image_check::full) == prefix + "unknown format"); CHECK(load_result(bad, image_check::full) == prefix + "unknown format");
+600
View File
@@ -0,0 +1,600 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_view;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <map>
#include <random>
#include <set>
#include <sstream>
#include <string>
#include <vector>
// These tests were split off unit-json_view_dump.cpp, whose object file got too large for the MinGW
// linker (see json_view_test_helpers.hpp). Like that file, this one is also built
// with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
for (const std::size_t members :
{
127u, 128u, 129u, 10000u
})
{
CAPTURE(members)
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\\n" : "") + "\":" + std::to_string(i);
}
text += R"(,"":"empty key","k1":"a duplicate of an earlier key"})";
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
for (std::size_t i = 0; i < members; ++i)
{
const std::string key = std::string(i % 23, 'k') + std::to_string(i) + (i % 7 == 0 ? "\n" : "");
CHECK(v[key].get<std::size_t>() == i);
CHECK(v.contains(key));
CHECK(v.find(key).key() == key);
CHECK(v.at(key).get<std::size_t>() == i);
CHECK(!v.contains(key + "x"));
}
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), json_view_test::message("out_of_range.403", "", 0, text.size(), "key 'missing' not found").c_str(), json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
SECTION("duplicate keys: the first member wins, with and without a table")
{
// an object of `total` members: the keys "k0".."k<n-1>" in order, then
// three keys repeated twice more (one copy in the middle, one at the
// end), and two keys repeated once; the value of a member is its
// position, so that the first member of a key can be told apart
struct member
{
std::string key;
std::size_t position;
};
const auto make_members = [](std::size_t total)
{
std::vector<std::string> keys;
for (std::size_t i = 0; i + 8 < total; ++i)
{
keys.push_back("k" + std::to_string(i));
}
const std::size_t n = keys.size();
const std::array<std::size_t, 3> triple = {{3, 17, n - 1}};
const std::array<std::size_t, 2> twice = {{5, n / 2}};
std::vector<std::string> ordered = keys;
for (const std::size_t i : triple)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 2), keys[i]);
}
for (const std::size_t i : twice)
{
ordered.insert(ordered.begin() + static_cast<std::ptrdiff_t>(ordered.size() / 3), keys[i]);
}
for (const std::size_t i : triple)
{
ordered.push_back(keys[i]);
}
std::vector<member> result;
result.reserve(ordered.size());
for (std::size_t i = 0; i < ordered.size(); ++i)
{
result.push_back({ordered[i], i});
}
return result;
};
// 100 and 127 members: no table; 128 members and more: a table
for (const std::size_t total :
{
100u, 127u, 128u, 200u, 5000u
})
{
CAPTURE(total)
const std::vector<member> members = make_members(total);
REQUIRE(members.size() >= total);
REQUIRE(members.size() >= 100);
std::string text = "{";
std::map<std::string, std::size_t> first;
std::map<std::string, std::size_t> last;
std::map<std::string, std::size_t> occurrences;
for (const member& m : members)
{
text += (text.size() > 1 ? ",\"" : "\"") + m.key + "\":" + std::to_string(m.position);
first.insert({m.key, m.position});
last[m.key] = m.position;
++occurrences[m.key];
}
text += '}';
REQUIRE(first.size() < members.size());
const json_document d = json_document::parse(text);
const json_view v = d.root();
const json j = json::parse(text);
CHECK(v.size() == members.size()); // every occurrence is visited
const json m = v.materialize();
for (const auto& entry : first)
{
CAPTURE(entry.first)
const std::size_t expected = entry.second;
CHECK(v[entry.first].get<std::size_t>() == expected);
CHECK(v.at(entry.first).get<std::size_t>() == expected);
CHECK(v.find(entry.first).value().get<std::size_t>() == expected);
CHECK(v.value(entry.first, std::size_t{0}) == expected);
CHECK(v.contains(entry.first));
CHECK(v.count(entry.first) == occurrences[entry.first]);
const std::string pointer = "/" + entry.first;
CHECK(v[json::json_pointer(pointer)].get<std::size_t>() == expected);
CHECK(v.at(json::json_pointer(pointer)).get<std::size_t>() == expected);
CHECK(v.value(json::json_pointer(pointer), std::size_t{0}) == expected);
CHECK(v.contains(json::json_pointer(pointer)));
// materialize() and parse() keep the last value instead
CHECK(j[entry.first].get<std::size_t>() == last.at(entry.first));
CHECK(m[entry.first].get<std::size_t>() == last.at(entry.first));
}
CHECK(!v.contains("k"));
CHECK(v["missing"].is_discarded());
CHECK(v.materialize() == j);
CHECK(v == j);
}
}
SECTION("colliding keys")
{
// the hash is not seeded, so keys that all land in one place must not
// make the table build quadratic: such an object gets no table and is
// searched linearly
constexpr std::size_t members = 300;
constexpr std::size_t table_size = 1024; // the table size for 300 members: the next power of two >= 600
std::vector<std::string> colliding;
std::vector<std::string> spread;
for (std::uint64_t counter = 0; colliding.size() < members || spread.size() < members; ++counter)
{
std::string key(8, 'a');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key[i] = static_cast<char>('a' + (x % 26));
}
const bool lands_in_slot_zero = (nlohmann::detail::view::key_hash(key.data(), key.size()) & (table_size - 1)) == 0;
if (lands_in_slot_zero && colliding.size() < members)
{
colliding.push_back(key);
}
else if (!lands_in_slot_zero && spread.size() < members)
{
spread.push_back(key);
}
}
const auto make_text = [](const std::vector<std::string>& keys)
{
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i % 10);
}
return text + "}";
};
const std::string colliding_text = make_text(colliding);
const std::string spread_text = make_text(spread);
json_document with_collisions = json_document::parse(colliding_text);
json_document without_collisions = json_document::parse(spread_text);
for (const auto* pair :
{
&colliding, &spread
})
{
const json_view v = (pair == &colliding ? with_collisions : without_collisions).root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[(*pair)[i]].get<std::size_t>() == i % 10);
CHECK(v.at((*pair)[i]).get<std::size_t>() == i % 10);
CHECK(v.find((*pair)[i]).key() == (*pair)[i]);
CHECK(!v.contains((*pair)[i] + "x"));
}
CHECK(!v.contains("missing"));
}
CHECK(with_collisions.root() == json::parse(colliding_text));
// only the object with the spread keys got a table (both texts have the
// same length, so the tables are the only difference)
with_collisions.shrink_to_fit();
without_collisions.shrink_to_fit();
CHECK(without_collisions.memory_usage() >= with_collisions.memory_usage() + (table_size * sizeof(std::uint32_t)));
}
SECTION("a lookup of a missing key stops at the end of the probe window")
{
// Every slot of the first half of the table holds a key, each at its home slot, so
// that the table is built; a key that is not in the object but whose home slot is
// the first slot finds a full window of slots with other keys.
constexpr std::size_t members = 128;
constexpr std::size_t table_size = 256; // the table size for 128 members: the next power of two >= 256
for (const std::size_t prefix_length :
{
std::size_t{0}, std::size_t{20}
})
{
CAPTURE(prefix_length)
std::vector<std::string> keys(members);
std::vector<bool> taken(members, false);
std::string missing;
std::size_t found = 0;
for (std::uint64_t counter = 0; found < members || missing.empty(); ++counter)
{
std::string key(prefix_length, 'p');
std::uint64_t x = counter;
for (std::size_t i = 0; i < 8; ++i, x /= 26)
{
key += static_cast<char>('a' + (x % 26));
}
const std::size_t slot = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (table_size - 1);
if (slot < members && !taken[slot])
{
taken[slot] = true;
keys[slot] = key;
++found;
}
else if (slot == 0 && missing.empty())
{
missing = key;
}
}
std::string text = "{";
for (std::size_t i = 0; i < members; ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
}
text += "}";
const json_document d = json_document::parse(text);
const json_view v = d.root();
for (std::size_t i = 0; i < members; ++i)
{
CAPTURE(i)
CHECK(v[keys[i]].get<std::size_t>() == i);
}
CHECK(!v.contains(missing));
CHECK(v.find(missing) == v.end());
CHECK(v.count(missing) == 0);
}
}
SECTION("keys that differ in their last bytes are spread over the table")
{
// The hash must spread the usual families of keys (numbered names, hex
// strings, dates, short names): a key farther than index_max_displacement
// from its home slot would leave the object without a table. The keys
// are placed with linear probing, as the table does.
const auto max_displacement = [](const std::vector<std::string>& keys)
{
std::size_t slots = 16;
while (slots < 2 * keys.size())
{
slots *= 2;
}
std::vector<bool> used(slots, false);
std::size_t farthest = 0;
for (const std::string& key : keys)
{
std::size_t i = nlohmann::detail::conditional_static_cast<std::size_t>(nlohmann::detail::view::key_hash(key.data(), key.size())) & (slots - 1);
std::size_t distance = 0;
while (used[i])
{
i = (i + 1) & (slots - 1);
++distance;
}
used[i] = true;
farthest = (std::max)(farthest, distance);
}
return farthest;
};
// the keys of a printf-like pattern with one %d, %0Nd or %0Nx, formatted with a stream
// (a format string that is not a literal is a Flawfinder error)
const auto numbered = [](const char* format, int count, int step)
{
const std::string pattern(format);
const std::size_t percent = pattern.find('%');
std::size_t conversion = percent + 1;
std::size_t width = 0;
while (pattern[conversion] >= '0' && pattern[conversion] <= '9')
{
width = (width * 10) + static_cast<std::size_t>(pattern[conversion] - '0');
++conversion;
}
const bool hex = pattern[conversion] == 'x';
std::vector<std::string> keys;
for (int i = 0; i < count; ++i)
{
std::ostringstream key;
key << pattern.substr(0, percent) << std::setfill('0') << std::setw(static_cast<int>(width))
<< (hex ? std::hex : std::dec) << (i * step) << pattern.substr(conversion + 1);
keys.push_back(key.str());
}
return keys;
};
for (const char* format :
{"key%d", "%d", "item_%04d", "a%05d", "%03dxyz", "id%d", "user_%d", "%d_suffix", "0x%04x", "A%07d", "12345678%d", "1234567%d", "123456789%d",
"configuration_parameter_value_%04d_extra", "2026-01-%02dT00:00:00Z"
})
{
CAPTURE(format)
CHECK(max_displacement(numbered(format, std::string(format).find("%02d") != std::string::npos ? 60 : 1000, 1)) < nlohmann::detail::view::index_max_displacement); // NOLINT(abseil-string-find-str-contains)
CHECK(max_displacement(numbered(format, 600, 7)) < nlohmann::detail::view::index_max_displacement);
}
std::mt19937_64 rng(7); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
for (const std::size_t length :
{
std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{5}, std::size_t{7}, std::size_t{8}, std::size_t{9}, std::size_t{12}, std::size_t{16}, std::size_t{17}, std::size_t{33}
})
{
CAPTURE(length)
std::set<std::string> distinct;
// (there are 26 and 676 keys of one and two letters)
std::size_t wanted = 700;
if (length == 1)
{
wanted = 26;
}
else if (length == 2)
{
wanted = 500;
}
while (distinct.size() < wanted)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
distinct.insert(key);
}
CHECK(max_displacement(std::vector<std::string>(distinct.begin(), distinct.end())) < nlohmann::detail::view::index_max_displacement);
}
}
SECTION("keys of every length that differ in one byte")
{
// In a large object, a key is found by comparing its bytes with those of the member the
// table points to: up to 16 bytes as two overlapping words, longer ones with memcmp.
// Keys of every length from 0 to 40, and keys that differ from them in one byte.
std::mt19937_64 rng(2026); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::vector<std::string> keys;
std::set<std::string> distinct;
for (std::size_t length = 0; length <= 40; ++length)
{
for (int copy = 0; copy < 5; ++copy)
{
std::string key(length, 'a');
for (char& c : key)
{
c = static_cast<char>('a' + (rng() % 26));
}
if (distinct.insert(key).second)
{
keys.push_back(key);
}
}
}
std::string text = "{";
for (std::size_t i = 0; i < keys.size(); ++i)
{
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
}
text += "}";
REQUIRE(keys.size() >= 128); // (a table is built for 128 members)
const json_document d = json_document::parse(text);
const json_view v = d.root();
for (std::size_t i = 0; i < keys.size(); ++i)
{
CAPTURE(keys[i])
CHECK(v[keys[i]].get<std::size_t>() == i);
CHECK(v.at(keys[i]).get<std::size_t>() == i);
CHECK(v.contains(keys[i]));
for (std::size_t at = 0; at < keys[i].size(); ++at)
{
std::string other = keys[i];
other[at] = other[at] == 'z' ? 'y' : 'z';
CAPTURE(other)
CHECK(v.contains(other) == (distinct.count(other) != 0));
}
CHECK(v.contains(keys[i] + "a") == (distinct.count(keys[i] + "a") != 0));
if (!keys[i].empty())
{
CHECK(v.contains(keys[i].substr(1)) == (distinct.count(keys[i].substr(1)) != 0));
}
}
}
SECTION("shrink_to_fit releases the tables' spare capacity")
{
const auto make_text = [](int objects, int members)
{
std::string text = "[";
for (int object = 0; object < objects; ++object)
{
text += object != 0 ? ",{" : "{";
for (int i = 0, n = members + object; i < n; ++i)
{
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
}
return text + "]";
};
const std::string small_text = make_text(5, 150);
const std::string big_text = make_text(40, 400);
// reading a big text, and then a small one, leaves the spare capacity
// of the big one: shrink_to_fit() brings the document to the size of
// one parsed from the small text alone
json_document d = json_document::parse(big_text);
const std::size_t big = d.memory_usage();
d.read(small_text);
CHECK(d.memory_usage() >= big);
d.shrink_to_fit();
json_document fresh = json_document::parse(small_text);
fresh.shrink_to_fit();
CHECK(d.memory_usage() == fresh.memory_usage());
CHECK(d.memory_usage() < big / 2);
CHECK(d.root() == json::parse(small_text));
for (int object = 0; object < 5; ++object)
{
const json_view v = d.root()[static_cast<std::size_t>(object)];
for (int i = 0, n = 150 + object; i < n; ++i)
{
CHECK(v[std::to_string(i)].get<int>() == i);
}
}
}
SECTION("nested, reused, and in arrays")
{
std::string inner = "{";
for (int i = 0; i < 300; ++i)
{
inner += (i != 0 ? ",\"m" : "\"m") + std::to_string(i) + "\":" + std::to_string(i);
}
inner += '}';
const std::string text = "[" + inner + ",{\"x\":" + inner + "}," + inner + "]";
json_document d = json_document::parse(text);
CHECK(d.root()[0]["m299"].get<int>() == 299);
CHECK(d.root()[1]["x"]["m150"].get<int>() == 150);
CHECK(d.root()[2]["m0"].get<int>() == 0);
const std::size_t with_index = d.memory_usage();
d.read(std::string("{\"small\": 1}"));
CHECK(d.root()["small"].get<int>() == 1);
d.read(text);
CHECK(d.root()[2]["m7"].get<int>() == 7);
CHECK(d.memory_usage() >= with_index / 2);
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
#endif #endif
// the view's own macros do not leak // the view's own macros do not leak
#if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) \ #if defined(NLOHMANN_VIEW_LIKELY) || defined(NLOHMANN_VIEW_UNLIKELY) || defined(NLOHMANN_VIEW_ALWAYS_INLINE) || defined(NLOHMANN_VIEW_NOINLINE) || defined(NLOHMANN_VIEW_NOCLONE) \
|| defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \ || defined(NLOHMANN_VIEW_NODISCARD) || defined(NLOHMANN_VIEW_THROW) || defined(NLOHMANN_VIEW_HAS_CPP_17) || defined(NLOHMANN_VIEW_LITTLE_ENDIAN) \
|| defined(NLOHMANN_VIEW_REPEAT16) || defined(NLOHMANN_VIEW_REPEAT16)
#error "json_view.hpp leaks a macro" #error "json_view.hpp leaks a macro"
+296
View File
@@ -20,6 +20,7 @@ using nlohmann::ordered_json;
using nlohmann::json_document; using nlohmann::json_document;
using nlohmann::json_editable_document; using nlohmann::json_editable_document;
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
@@ -81,6 +82,112 @@ std::string range(const json& j)
{ {
return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos()); return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos());
} }
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
// the exceptions about every value of v are those about the value at the same
// place in j, a value of the whole document from parse() or materialize() (so
// that its parents and positions are set); members whose key repeats are
// skipped (the view finds the first one, j holds the last)
template<typename View>
void check_exceptions(const View& v, const json& j)
{
CAPTURE(j.dump())
if (!v.is_number())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<int>()); }) == exception_of_call([&] { static_cast<void>(j.get<int>()); }));
}
if (!v.is_string())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::string>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::string>()); }));
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::vector<int>>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::vector<int>>()); }));
}
CHECK(exception_of_call([&] { static_cast<void>(v.at("missing")); }) == exception_of_call([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(1000)); }) == exception_of_call([&] { static_cast<void>(j.at(1000)); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/missing"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/missing"))); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/-"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/-"))); }));
CHECK(exception_of_call([&] { static_cast<void>(v.value("k", 0)); }) == exception_of_call([&] { static_cast<void>(j.value("k", 0)); }));
if (!v.is_object())
{
CHECK(exception_of_call([&] { static_cast<void>(v["k"]); }) == exception_of_call([&] { static_cast<void>(j["k"]); }));
// (without the context of basic_json: view iterators do not know the value they iterate over)
CHECK(exception_of_call([&] { static_cast<void>(v.begin().key()); }) == "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { static_cast<void>(v[0]); }) == exception_of_call([&] { static_cast<void>(j[0]); }));
}
if (v.is_null())
{
CHECK(exception_of_call([&] { static_cast<void>(v.front()); }) == exception_of_call([&] { static_cast<void>(j.front()); }));
}
if (v.is_object())
{
for (auto it = v.begin(); it != v.end(); ++it)
{
const std::string key(it.key().data(), it.key().size());
if (v.count(key) == 1)
{
check_exceptions(it.value(), j.at(key));
}
}
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i)
{
check_exceptions(v[i], j[i]);
}
}
}
// as check_exceptions, for edits that fail (they change neither d nor j)
void check_edit_exceptions(json_editable_document& d, const json_editable_document::view_type& v, json& j)
{
CAPTURE(j.dump())
if (!v.is_object() && !v.is_null())
{
CHECK(exception_of_call([&] { d.set(v, "k", 1); }) == exception_of_call([&] { j["k"] = 1; }));
CHECK(exception_of_call([&] { d.erase(v, "k"); }) == exception_of_call([&] { j.erase("k"); }));
}
if (!v.is_array() && !v.is_null())
{
CHECK(exception_of_call([&] { d.set(v, 0, 1); }) == exception_of_call([&] { j[0] = 1; }));
CHECK(exception_of_call([&] { d.push_back(v, 1); }) == exception_of_call([&] { j.push_back(1); }));
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { d.insert(v, 0, 1); }) == exception_of_call([&] { j.insert(j.cbegin(), 1); }));
CHECK(exception_of_call([&] { d.erase(v, 0); }) == exception_of_call([&] { j.erase(0); }));
}
else
{
CHECK(exception_of_call([&] { d.erase(v, 1000); }) == exception_of_call([&] { j.erase(1000); }));
}
if (v.is_object())
{
for (auto it = v.begin(); it != v.end(); ++it)
{
const std::string key(it.key().data(), it.key().size());
if (v.count(key) == 1)
{
check_edit_exceptions(d, it.value(), j.at(key));
}
}
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i)
{
check_edit_exceptions(d, v[i], j[i]);
}
}
}
#endif
} // namespace } // namespace
TEST_CASE("json_view materialize() source positions") TEST_CASE("json_view materialize() source positions")
@@ -321,6 +428,31 @@ TEST_CASE("json_view materialize() source positions")
CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2])); CHECK(same_positions(m[std::string("a") + '\n'][2], j[std::string("a") + '\n'][2]));
} }
SECTION("a damaged text: the end of a container is not found")
{
// (the text of an image is not checked with image_check::none)
const std::string array_text = "[1,2]";
const std::vector<std::uint8_t> original = json_document::parse(array_text).save();
const auto at = std::search(original.begin(), original.end(), array_text.begin(), array_text.end());
REQUIRE(at != original.end());
const auto closing = static_cast<std::size_t>(at - original.begin()) + array_text.size() - 1;
// not a closing bracket, a slash that starts no comment, and the end of the text
for (const char replacement :
{
'x', '/', ' '
})
{
CAPTURE(replacement)
std::vector<std::uint8_t> damaged = original;
damaged[closing] = static_cast<std::uint8_t>(replacement);
const json_document d = json_document::load(damaged, json_document::image_check::none);
const json m = d.root().materialize();
CHECK(range(m) == "none");
CHECK(range(m[0]) == "1-2");
CHECK(range(m[1]) == "3-4");
}
}
#if !defined(JSON_NOEXCEPTION) #if !defined(JSON_NOEXCEPTION)
SECTION("a decoded string's position must be within the text") SECTION("a decoded string's position must be within the text")
{ {
@@ -345,3 +477,167 @@ TEST_CASE("json_view materialize() source positions")
} }
#endif #endif
} }
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view exceptions name the path and the bytes of the value")
{
SECTION("as those of basic_json")
{
const std::vector<std::string> texts =
{
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
R"({"a\u0000b": [1, "x"], "c": {"d\u0000": 2}})", // (the path of a key ends at a null byte, as in basic_json)
R"([1 /* ] */, {"a": [] // }
, "b": {} } , ])",
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
};
for (const auto& text : texts)
{
CAPTURE(text)
const json j = json::parse(text, nullptr, true, true, true);
const json_document d = json_document::parse(text, true, true, true);
check_exceptions(d.root(), j);
json_editable_document e = json_editable_document::parse(text, true, true, true);
check_exceptions(e.root(), j);
json m = e.root().materialize();
check_edit_exceptions(e, e.root(), m);
}
}
SECTION("generated documents")
{
json_view_test::generator gen;
for (int i = 0; i < 100; ++i)
{
std::string text;
gen.value(text, 0);
CAPTURE(text)
const json_document d = json_document::parse(text);
check_exceptions(d.root(), json::parse(text));
}
}
SECTION("edited documents: as those of materialize()")
{
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
json_editable_document d = json_editable_document::parse(text);
d.set(d.root()["f"], 5);
d.push_back(d.root()["a"], json::parse(R"({"n": [null]})"));
d.erase(d.root()["e"], 0);
d.set(d.root()["b"], "c2", d.root()["e"]);
d.insert(d.root()["e"], 0, "new");
json m = d.root().materialize();
check_exceptions(d.root(), m);
check_edit_exceptions(d, d.root(), m);
// (a value inside a new container: a path, but no bytes)
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/a/2/n/0) ";
#endif
expected += "type must be number, but is null";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
}
SECTION("views of values that are no longer part of the document, and of no value")
{
const std::string expected_string = "[json.exception.type_error.302] type must be number, but is string";
static_cast<void>(expected_string);
// (a value that is not found has no path, and with it no bytes)
{
json_editable_document d = json_editable_document::parse(R"(["x", ["y"]])");
const auto removed = d.root()[0];
d.erase(d.root(), 0);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
// (the root is a scalar now: the value cannot be in it)
d.set(d.root(), 5);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
CHECK(d.root().get<int>() == 5);
}
{
json_editable_document d = json_editable_document::parse(R"(["x"])");
const auto removed = d.root()[0];
d.set(d.root(), 5);
#if JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(removed.get<int>()); }) == expected_string);
#endif
}
// a view of no value has no context
CHECK(exception_of_call([&] { static_cast<void>(nlohmann::json_view().get<int>()); }) == "[json.exception.type_error.302] type must be number, but is discarded");
}
SECTION("an array whose last element was replaced by a new container has no bytes")
{
json_editable_document d = json_editable_document::parse(R"({"a": [1, [2]], "b": 1})");
d.set(d.root()["a"][1], json::array({3}));
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/a) ";
#endif
expected += "type must be number, but is array";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"].get<int>()); }) == expected);
}
SECTION("deep documents with siblings after every nested container")
{
// the search for the path pushes and pops one frame per level (the
// stack reallocates while it grows, and shrinks again at every
// sibling): run under a sanitizer, a reference to a popped or moved
// frame would show here
std::string text = "7";
for (int level = 0; level < 40; ++level)
{
text = R"([[1, [2]], )" + text + R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
}
CAPTURE(text)
const json j = json::parse(text);
const json_document d = json_document::parse(text);
check_exceptions(d.root(), j);
json_editable_document e = json_editable_document::parse(text);
e.set(e.root()[0][0], 9);
check_exceptions(e.root(), e.root().materialize());
}
SECTION("examples")
{
const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})";
const json_document d = json_document::parse(text);
std::string expected_age = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected_age += "(/people/1/age) ";
#endif
expected_age += "(bytes 58-61) type must be number, but is string";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["people"][1]["age"].get<int>()); }) == expected_age);
std::string expected_zz = "[json.exception.out_of_range.403] ";
#if JSON_DIAGNOSTICS
expected_zz += "(/people/0) ";
#endif
expected_zz += "(bytes 12-35) key 'zz' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json)
std::string expected_missing = "[json.exception.out_of_range.403] ";
#if !JSON_DIAGNOSTICS
expected_missing += "(bytes 0-79) ";
#endif
expected_missing += "key 'missing' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
}
SECTION("duplicate keys: the value the view finds")
{
const json_document d = json_document::parse(R"({"x": "1", "x": "22"})");
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/x) ";
#endif
expected += "(bytes 6-9) type must be number, but is string";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["x"].get<int>()); }) == expected);
}
}
#endif
+53
View File
@@ -12,7 +12,10 @@
using nlohmann::json; using nlohmann::json;
#include <algorithm> #include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string> #include <string>
#include <utility>
TEST_CASE("tests on very large JSONs") TEST_CASE("tests on very large JSONs")
{ {
@@ -140,6 +143,56 @@ TEST_CASE("tests on deeply nested JSONs")
} }
} }
SECTION("leaf values of every type below the bound of the recursive descent")
{
// the elements and members of the containers below the bound are
// copied one by one, whatever their type
const json leaves = json::array(
{
nullptr, true, false, -1, std::uint64_t{1}, 1.5, "text", json::binary({1, 2, 3})
});
const json members =
{
{"null", nullptr}, {"boolean", true}, {"integer", -1}, {"unsigned", std::uint64_t{1}},
{"float", 1.5}, {"string", "text"}, {"binary", json::binary({1, 2, 3})}
};
for (const bool objects :
{
false, true
})
{
CAPTURE(objects)
json j = objects ? members : leaves;
for (std::size_t i = 0; i < 300; ++i)
{
json wrapper = objects ? json::object() : json::array();
if (objects)
{
wrapper["a"] = std::move(j);
}
else
{
wrapper.push_back(std::move(j));
}
j = std::move(wrapper);
}
const json copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is what is tested
const json* current = &copy;
for (std::size_t i = 0; i < 300; ++i)
{
current = objects ? &current->at("a") : &current->front();
}
// the innermost value is shallow: it is safe to compare
CHECK(*current == (objects ? members : leaves));
CHECK(current->size() == (objects ? members.size() : leaves.size()));
}
}
SECTION("a value that is deep in one place only") SECTION("a value that is deep in one place only")
{ {
json j = json::object(); json j = json::object();
+203
View File
@@ -495,3 +495,206 @@ TEST_CASE("locale changes during a single dump() (#5709 item 3)")
// fix #5597 made on the parser side // fix #5597 made on the parser side
CHECK(buf.data == "[\"" + padding + "\",1234.5]"); CHECK(buf.data == "[\"" + padding + "\",1234.5]");
} }
namespace
{
// a token whose first data() call switches LC_NUMERIC, so that the locale
// changes between convert_float_locale_aware() looking up the decimal point
// and strtod reading the token (the race of #5198, made deterministic)
struct locale_switching_token
{
using value_type = char;
locale_switching_token(std::string text_, const char* switch_to_)
: text(std::move(text_)), switch_to(switch_to_)
{}
std::size_t size() const
{
return text.size();
}
char& operator[](std::size_t i)
{
return text[i];
}
char* data()
{
if (!switched)
{
switched = std::setlocale(LC_NUMERIC, switch_to) != nullptr;
}
return &text[0];
}
std::string text;
const char* switch_to;
bool switched = false;
};
// set LC_NUMERIC to the first of @a names that is usable and has a decimal
// point of @a min_size bytes at least (and at most @a max_size)
bool use_decimal_point_locale(const std::vector<const char*>& names, std::size_t min_size, std::size_t max_size, const char*& used)
{
for (const char* name : names)
{
if (std::setlocale(LC_NUMERIC, name) == nullptr)
{
continue;
}
const std::size_t size = std::string(std::localeconv()->decimal_point).size();
if (size >= min_size && size <= max_size)
{
used = name;
return true;
}
}
return false;
}
// convert a token with convert_float_locale_aware(); the token must be given
// back as it was
template<typename FloatType>
FloatType convert_locale_aware(const std::string& text)
{
std::string token = text;
const std::size_t dot = token.find('.');
FloatType value{};
nlohmann::detail::convert_float_locale_aware(token, dot, value);
CHECK(token == text);
return value;
}
template<typename FloatType>
void check_locale_aware_conversions()
{
CHECK(convert_locale_aware<FloatType>("3.5") == FloatType(3.5));
CHECK(convert_locale_aware<FloatType>("-0.25") == FloatType(-0.25));
CHECK(convert_locale_aware<FloatType>("1.25e2") == FloatType(125));
// no decimal point at all
CHECK(convert_locale_aware<FloatType>("12e1") == FloatType(120));
CHECK(convert_locale_aware<FloatType>("7") == FloatType(7));
}
} // namespace
TEST_CASE("convert_float_locale_aware")
{
const std::vector<const char*> single_byte_names = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", "fr_FR", "ru_RU.UTF-8"};
const std::vector<const char*> multi_byte_names = {"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"};
const char* used = nullptr;
SECTION("decimal point '.'")
{
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
check_locale_aware_conversions<float>();
check_locale_aware_conversions<double>();
check_locale_aware_conversions<long double>();
}
SECTION("single-byte decimal point other than '.'")
{
if (use_decimal_point_locale(single_byte_names, 1, 1, used) && std::localeconv()->decimal_point[0] != '.')
{
CAPTURE(used)
check_locale_aware_conversions<float>();
check_locale_aware_conversions<double>();
check_locale_aware_conversions<long double>();
}
else
{
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
}
}
SECTION("multi-byte decimal point")
{
if (use_decimal_point_locale(multi_byte_names, 2, 8, used))
{
CAPTURE(used)
check_locale_aware_conversions<float>();
check_locale_aware_conversions<double>();
check_locale_aware_conversions<long double>();
}
else
{
MESSAGE("no locale with a multi-byte decimal point is usable");
}
}
SECTION("a token that is not converted completely")
{
// the locale does not change, so there is nothing to retry: the
// value up to the garbage is kept
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
std::string token = "1.5x";
double value = 0;
nlohmann::detail::convert_float_locale_aware(token, token.find('.'), value);
CHECK(value == 1.5);
CHECK(token == "1.5x");
}
SECTION("the locale changes before the conversion")
{
if (!use_decimal_point_locale(single_byte_names, 1, 1, used) || std::localeconv()->decimal_point[0] == '.')
{
MESSAGE("no locale with a single-byte decimal point other than '.' is usable");
}
else
{
const std::string comma_locale = used;
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
// "C" -> comma locale: strtod stops at the '.', so the conversion is repeated
{
locale_switching_token token("1.5", comma_locale.c_str());
double value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 1.5);
CHECK(token.text == "1.5");
}
}
// the same for the other floating-point types
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
{
locale_switching_token token("2.25", comma_locale.c_str());
float value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 2.25f);
}
}
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
{
locale_switching_token token("2.25", comma_locale.c_str());
long double value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 2.25L);
}
}
// comma locale -> multi-byte decimal point
const char* multi_byte = nullptr;
if (use_decimal_point_locale(multi_byte_names, 2, 8, multi_byte))
{
REQUIRE(std::setlocale(LC_NUMERIC, comma_locale.c_str()) != nullptr);
locale_switching_token token("1.5", multi_byte);
double value = 0;
nlohmann::detail::convert_float_locale_aware(token, 1, value);
if (token.switched)
{
CHECK(value == 1.5);
CHECK(token.text == "1.5");
}
}
}
}
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
}
+30
View File
@@ -18,6 +18,7 @@ using nlohmann::json;
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
#include <cstddef> #include <cstddef>
#include <string>
#include <vector> #include <vector>
// Test suite for verifying MessagePack handling with std::byte input // Test suite for verifying MessagePack handling with std::byte input
@@ -96,4 +97,33 @@ TEST_CASE("MessagePack with std::byte")
} }
} }
} }
// JSON text (not MessagePack) as std::byte input: the error position is
// computed from the bytes consumed so far
TEST_CASE("JSON text with std::byte")
{
const auto to_bytes = [](const std::string & text)
{
std::vector<std::byte> bytes;
for (const char c : text)
{
bytes.push_back(static_cast<std::byte>(c));
}
return bytes;
};
SECTION("valid text")
{
const json j = json::parse(to_bytes("[1,\n2]"));
CHECK(j == json({1, 2}));
CHECK(json::accept(to_bytes("[1,\n2]")));
}
SECTION("invalid text")
{
CHECK_FALSE(json::accept(to_bytes("[1,\n2,")));
CHECK_THROWS_WITH_AS(json::parse(to_bytes("[1,\n2,")),
"[json.exception.parse_error.101] parse error at line 2, column 3: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", json::parse_error&);
}
}
#endif #endif
+99
View File
@@ -27,12 +27,15 @@ using nlohmann::json;
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <limits> #include <limits>
#include <list>
#include <set> #include <set>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp" #include "round_trip_corpus.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#include "sax_countdown.hpp" #include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown; using utils::SaxCountdown;
using utils::SaxEventLogger;
TEST_CASE("MessagePack") TEST_CASE("MessagePack")
@@ -2212,3 +2215,99 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)")
} }
} }
} }
TEST_CASE("MessagePack arrays of scalars from contiguous and non-contiguous input")
{
// contiguous input reads the integers, floats, booleans, and nils an
// array begins with in one loop, other input value by value; both must
// give the same events, value, or error for the complete document and for
// every truncation of it, and stop at every event a SAX handler rejects
const std::vector<std::uint8_t> msgpack =
{
0x83, // map(3)
0xA1, 'a', // "a"
0xDC, 0x00, 0x16, // array 16 (22)
0x00, 0x7F, 0xCC, 0xFF, 0xCD, 0x01, 0x00, // 0, 127, 255, 256
0xCE, 0x00, 0x01, 0x00, 0x00, // 65536
0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 18446744073709551615
0xFF, 0xE0, 0xD0, 0x80, 0xD1, 0xFF, 0x00, // -1, -32, -128, -256
0xD2, 0xFF, 0xFE, 0xFF, 0xFF, // -65537
0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -9223372036854775808
0xC2, 0xC3, 0xC0, // false, true, nil
0xCA, 0x3F, 0xC0, 0x00, 0x00, // 1.5 (float 32)
0xCB, 0xBF, 0xB9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, // -0.1
0xA1, 'x', // "x" (ends the loop)
0x01, // 1
0xCB, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2.5
0xD9, 0x01, 'y', // "y" (str 8)
0xC0, // nil
0xA1, 'b', // "b"
0x93, // fixarray(3)
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x92, 0xCB, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // [1.25,
0xCB, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -3.75]
0x90, // []
0xA1, 'c', // "c"
0x94, 0x80, 0x01, 0xC4, 0x01, 0x05, 0x91, 0x03 // [{}, 1, bin 8, [3]]
};
const json j = json::from_msgpack(msgpack);
CHECK(j["a"].size() == 22);
CHECK(j["a"][5] == 18446744073709551615u);
CHECK(j["a"][7] == -32);
CHECK(j["a"][11] == (std::numeric_limits<std::int64_t>::min)());
CHECK(j["a"][15] == 1.5);
CHECK(j["a"][16] == -0.1);
CHECK(j["a"][17] == "x");
CHECK(j["b"] == json::parse("[[1.25, -3.75], [1.25, -3.75], []]"));
CHECK(j["c"][2].is_binary());
for (std::size_t length = 0; length <= msgpack.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(msgpack.begin(), msgpack.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_msgpack(contiguous, true, false).dump() == json::from_msgpack(bytewise.begin(), bytewise.end(), true, false).dump());
#if !defined(JSON_NOEXCEPTION)
std::string contiguous_error;
std::string bytewise_error;
try
{
json _;
_ = json::from_msgpack(contiguous);
}
catch (const json::parse_error& e)
{
contiguous_error = e.what();
}
try
{
json _;
_ = json::from_msgpack(bytewise.begin(), bytewise.end());
}
catch (const json::parse_error& e)
{
bytewise_error = e.what();
}
CHECK(contiguous_error == bytewise_error);
#endif
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::msgpack)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 60; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(msgpack.begin(), msgpack.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(msgpack, &from_contiguous, json::input_format_t::msgpack)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::msgpack));
}
}
+85
View File
@@ -16,6 +16,7 @@ using nlohmann::ordered_json;
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <vector>
TEST_CASE("ordered_json") TEST_CASE("ordered_json")
{ {
@@ -515,3 +516,87 @@ TEST_CASE("parsing random trees of arrays and objects")
CHECK(json::parse(text).dump() == expected.dump()); CHECK(json::parse(text).dump() == expected.dump());
} }
} }
TEST_CASE("object_key_index")
{
// the hash table of the DOM parser finds the position of a key in an
// ordered_json object; the parser creates it for exactly 64 elements, but
// it is correct for any number of elements it is given
using nlohmann::detail::object_key_index;
const std::size_t not_found = object_key_index::npos;
for (const std::size_t initial :
{
std::size_t{0}, std::size_t{10}, std::size_t{64}, std::size_t{65}, std::size_t{300}
})
{
CAPTURE(initial)
// the hashes are not distinct on purpose: elements with the same
// hash are told apart by the caller
const auto hash_of = [](std::size_t position)
{
return (position % 50) * 0x9E3779B97F4A7C15ull;
};
std::vector<std::size_t> hashes;
for (std::size_t i = 0; i < initial; ++i)
{
hashes.push_back(hash_of(i));
}
object_key_index index(std::move(hashes));
// the elements that are there
for (std::size_t i = 0; i < initial; ++i)
{
CHECK(index.find(hash_of(i), [i](std::size_t position)
{
return position == i;
}) == i);
}
CHECK(index.find(hash_of(1), [](std::size_t /*position*/)
{
return false;
}) == not_found);
// elements that are added later, until the table grows
for (std::size_t i = initial; i < initial + 600; ++i)
{
index.add(hash_of(i));
CHECK(index.find(hash_of(i), [i](std::size_t position)
{
return position == i;
}) == i);
}
CHECK(index.find(hash_of(initial), [initial](std::size_t position)
{
return position == initial;
}) == initial);
}
}
TEST_CASE("JSON pointers on ordered_json")
{
using pointer = ordered_json::json_pointer;
SECTION("operator[] inserts a missing key at the end")
{
ordered_json j = {{"z", 1}, {"x", {{"y", 2}}}};
j[pointer("/a")] = 3;
j[pointer("/x/b")] = 4;
CHECK(j == ordered_json({{"z", 1}, {"x", {{"y", 2}, {"b", 4}}}, {"a", 3}}));
CHECK(j.dump() == R"({"z":1,"x":{"y":2,"b":4},"a":3})");
}
SECTION("unflatten of a flattened object whose keys are not sorted")
{
// the prefixes /a and /c denote arrays; a key of /a follows one of /c
const ordered_json flat = {{"/a/0/p", 1}, {"/c/0/x", 2}, {"/a/0/q", 3}, {"/a/1/p", 4}};
const ordered_json expected = {{"a", {{{"p", 1}, {"q", 3}}, {{"p", 4}}}}, {"c", {{{"x", 2}}}}};
CHECK(flat.unflatten() == expected);
CHECK(expected.flatten().unflatten() == expected);
}
}
+71
View File
@@ -23,11 +23,13 @@ using nlohmann::json;
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
#endif #endif
#include <array>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <list> #include <list>
#include <limits> #include <limits>
#include <cstdio> #include <cstdio>
#include <utility>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
@@ -1519,6 +1521,75 @@ TEST_CASE("regression tests 1")
} }
} }
SECTION("operator[](json_pointer) continues with the member that emplace() found")
{
// see the section above: the lookup does not find "k", but emplace()
// finds "x" and the pointer continues with the member of "x". Here
// the member is a container, so the pointer can go on below it. Only
// the outermost object may be a copy (with restarted timestamps): the
// value of "x" is moved in, so that its own lookups work.
const my_json orig = my_json::parse(R"({"x":null})");
const auto make = [&orig]()
{
my_json::object_t top = orig.get_ref<const my_json::object_t&>();
top.at("x") = my_json::parse(R"({"a":{"b":1},"n":null,"arr":[10,20],"objs":[{"m":5},{"m":6}]})");
return my_json(std::move(top));
};
SECTION("existing members")
{
// emplace() registers the key "k" in the object, so that every
// pointer needs a new object to reach the member of "x" again
const std::array<std::pair<const char*, const char*>, 4> pointers = {{{"/k/a/b", "1"}, {"/k/arr/1", "20"}, {"/k/n", "null"}, {"/k/objs/1/m", "6"}}};
for (const auto& pointer : pointers)
{
CAPTURE(pointer.first)
my_json j = make();
CHECK(j[my_json::json_pointer(pointer.first)].dump() == pointer.second);
CHECK(j.size() == 1);
CHECK(j["x"].size() == 4);
}
}
SECTION("a missing member of an object")
{
my_json j = make();
#ifdef JSON_NO_THREAD_LOCAL
// the objects below the outermost one are not copied by their
// object_t here, but member by member, which keeps their
// timestamps correct: "c" is not mistaken for "b" and is inserted
CHECK(j[my_json::json_pointer("/k/a/c/d")].is_null());
CHECK(j["x"]["a"].dump() == R"({"b":1,"c":{"d":null}})");
#else
// the timestamp of "c" is that of "b" again, so that emplace()
// finds the member "b", a number, which has no member "d"
CHECK_THROWS_AS(j[my_json::json_pointer("/k/a/c/d")], my_json::out_of_range&);
CHECK(j["x"]["a"].dump() == R"({"b":1})");
#endif
}
SECTION("a null value is converted to an object")
{
my_json j = make();
j[my_json::json_pointer("/k/n/c")] = 3;
CHECK(j["x"]["n"].dump() == R"({"c":3})");
}
SECTION("the end of an array")
{
my_json j = make();
j[my_json::json_pointer("/k/arr/-")] = 30;
CHECK(j["x"]["arr"].dump() == "[10,20,30]");
}
SECTION("an index past the end of an array")
{
my_json j = make();
j[my_json::json_pointer("/k/arr/3")] = 40;
CHECK(j["x"]["arr"].dump() == "[10,20,null,40]");
}
}
SECTION("issue #977 - Assigning between different json types") SECTION("issue #977 - Assigning between different json types")
{ {
foo_json lj = ns::foo{3}; foo_json lj = ns::foo{3};
+4
View File
@@ -441,6 +441,10 @@ TEST_CASE("regression tests 2")
float_json j2 = {1000.0, 2000.0, 3000.0}; float_json j2 = {1000.0, 2000.0, 3000.0};
CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2); CHECK(float_json::from_ubjson(float_json::to_ubjson(j2, true, true)) == j2);
// half-precision floats of both signs are converted to the float type
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0xC0, 0x00}) == -2.0f);
CHECK(float_json::from_cbor(std::vector<std::uint8_t> {0xF9, 0x3C, 0x00}) == 1.0f);
} }
SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?") SECTION("issue #1045 - Using STL algorithms with JSON containers with expected results?")
+46
View File
@@ -23,6 +23,7 @@ using nlohmann::json;
#include <cstdint> #include <cstdint>
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <limits>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -248,6 +249,51 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3
} }
} }
TEST_CASE("integers of every digit count are written like std::to_string")
{
// dump_integer() writes up to 16 digits in two groups of eight and 17 to
// 20 digits as one to four digits and two groups of eight
std::vector<std::uint64_t> values;
std::uint64_t power = 1;
for (int digits = 1; digits <= 20; ++digits)
{
values.push_back(power); // 1 followed by zeros
values.push_back(power + 1);
values.push_back((power * 2) - 1);
if (digits < 20)
{
values.push_back((power * 10) - 1); // all 9s
values.push_back((power * 10) - power); // 9 followed by zeros
power *= 10;
}
}
values.push_back((std::numeric_limits<std::uint64_t>::max)());
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()));
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1);
std::uint64_t state = 0x9E3779B97F4A7C15u;
for (int i = 0; i < 20000; ++i)
{
state = (state * 6364136223846793005u) + 1442695040888963407u;
values.push_back(state >> (state % 61)); // every bit length
}
for (const auto v : values)
{
CAPTURE(v)
CHECK(json(v).dump() == std::to_string(v));
if (v <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
{
const auto s = static_cast<std::int64_t>(v);
CHECK(json(s).dump() == std::to_string(s));
CHECK(json(-s).dump() == std::to_string(-s));
}
}
CHECK(json((std::numeric_limits<std::int64_t>::min)()).dump() == "-9223372036854775808");
// in a container, so the digits are followed by other output
CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]");
}
TEST_CASE("dump with binary values") TEST_CASE("dump with binary values")
{ {
auto binary = json::binary({1, 2, 3, 4}); auto binary = json::binary({1, 2, 3, 4});
+58
View File
@@ -19,11 +19,13 @@
using nlohmann::json; using nlohmann::json;
#include <algorithm> #include <algorithm>
#include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <fstream> #include <fstream>
#include <istream> #include <istream>
#include <limits>
#include <random> #include <random>
#include <sstream> #include <sstream>
#include <streambuf> #include <streambuf>
@@ -93,6 +95,33 @@ class chunked_streambuf : public std::streambuf
std::size_t m_unbuffered; std::size_t m_unbuffered;
}; };
#if TEST_STREAM_BULK_SCAN
// A streambuf that claims a get area of several gigabytes, of which only the
// first bytes exist. It is only used to move the read position of the adapter,
// never to read beyond them, so that skipping more than gbump() can bump at
// once can be tested without allocating that much.
class oversized_get_area_streambuf : public std::streambuf
{
public:
explicit oversized_get_area_streambuf(std::size_t claimed_size)
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
setg(m_data.data(), m_data.data(), m_data.data() + claimed_size);
}
// Give the get area back its real size, so that the adapter may read the
// character at the read position when it is destroyed (with
// JSON_PRECISE_STREAM_POSITION, it consumes the last character returned).
void restore_get_area()
{
setg(m_data.data(), m_data.data(), m_data.data() + m_data.size());
}
private:
std::array<char, 8> m_data{{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}};
};
#endif
// how a test reads from the stream // how a test reads from the stream
enum class read_mode enum class read_mode
{ {
@@ -433,6 +462,35 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk")
ia.copy_token(token, false); ia.copy_token(token, false);
CHECK(token.empty()); CHECK(token.empty());
} }
SECTION("skipping more characters than gbump() takes at once")
{
// gbump() takes an int, but a get area can be larger
const std::size_t claimed = sizeof(std::size_t) < 8 ? 0 : std::size_t{3} << 30u;
oversized_get_area_streambuf buf(claimed);
if (claimed == 0 || static_cast<std::size_t>(buf.in_avail()) != claimed)
{
// a 32-bit size_t, or a standard library whose streambuf stores
// the size of the get area in an int (MSVC)
MESSAGE("a get area larger than an int is not supported");
}
else
{
std::istream is(&buf);
nlohmann::detail::input_stream_adapter ia(is);
CHECK(ia.get_character() == 'a');
CHECK(ia.bulk_remaining() == claimed - 1);
const std::size_t skipped = static_cast<std::size_t>((std::numeric_limits<int>::max)()) + 10;
ia.bulk_skip(skipped);
CHECK(ia.bulk_remaining() == claimed - 1 - skipped);
ia.bulk_skip(ia.bulk_remaining());
CHECK(ia.bulk_remaining() == 0);
buf.restore_get_area();
}
}
#endif #endif
SECTION("SAX parsing from a stream") SECTION("SAX parsing from a stream")
+46
View File
@@ -928,3 +928,49 @@ TEST_CASE("short decimals")
} }
} }
} }
TEST_CASE("decimals of up to 17 digits")
{
// write_decimal() lays out digits * 10^exp as format_buffer() does; to_chars()
// calls it for the doubles whose digits write_shortest() does not convert at
// once, and the digits may be as many as 17
const auto written = [](std::uint64_t digits, int exp) -> std::string
{
std::array<char, 64> buf{}; // (up to 41 bytes are written)
char* const end = nlohmann::detail::dtoa_impl::write_decimal(buf.data(), digits, exp);
return {buf.data(), end};
};
SECTION("fixed notation")
{
CHECK(written(1, 0) == "1.0");
CHECK(written(1234, -2) == "12.34");
CHECK(written(123, 2) == "12300.0");
CHECK(written(50000000000u, -1) == "5000000000.0");
CHECK(written(100000000u, -8) == "1.0");
CHECK(written(12345678u, -12) == "1.2345678e-05");
CHECK(written(1234, 11) == "123400000000000.0");
// the point moves left of the first digit
CHECK(written(5, -3) == "0.005");
CHECK(written(12345678901234567u, -20) == "0.00012345678901234567");
}
SECTION("exponent notation")
{
CHECK(written(5, -5) == "5e-05");
CHECK(written(1234, 12) == "1.234e+15");
CHECK(written(5, -324) == "5e-324");
}
SECTION("17 digits")
{
CHECK(written(12345678901234567u, -16) == "1.2345678901234567");
CHECK(written(12345678901234567u, -19) == "0.0012345678901234567");
CHECK(written(12345678901234567u, 5) == "1.2345678901234567e+21");
CHECK(written(99999999999999999u, -17) == "0.99999999999999999");
CHECK(written(10000000000000001u, -16) == "1.0000000000000001");
// trailing zeros are not written
CHECK(written(10000000000000000u, -17) == "0.1");
CHECK(written(12000000000000000u, -15) == "12.0");
}
}
+97
View File
@@ -21,12 +21,15 @@ using nlohmann::json;
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <list>
#include <set> #include <set>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp" #include "round_trip_corpus.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#include "sax_countdown.hpp" #include "sax_countdown.hpp"
#include "sax_event_loggers.hpp"
using utils::SaxCountdown; using utils::SaxCountdown;
using utils::SaxEventLogger;
TEST_CASE("UBJSON") TEST_CASE("UBJSON")
@@ -1670,6 +1673,31 @@ TEST_CASE("UBJSON")
return true; return true;
}); });
CHECK_THROWS_AS(scp2.start_array((std::numeric_limits<std::size_t>::max)() - 1), json::out_of_range&); CHECK_THROWS_AS(scp2.start_array((std::numeric_limits<std::size_t>::max)() - 1), json::out_of_range&);
// without exceptions, the error is reported to the caller
json l;
nlohmann::detail::json_sax_dom_callback_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> scp3(l, [](int /*unused*/, json::parse_event_t /*unused*/, const json& /*unused*/) noexcept
{
return true;
}, false);
CHECK_FALSE(scp3.start_array((std::numeric_limits<std::size_t>::max)() - 1));
CHECK(scp3.is_errored());
}
SECTION("array with a known size, read without exceptions")
{
std::vector<uint8_t> const v_ubjson = {'[', '#', 'i', 2, 'i', 1, 'i', 2};
json j;
nlohmann::detail::json_sax_dom_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> sax(j, false);
CHECK(json::sax_parse(v_ubjson, &sax, json::input_format_t::ubjson));
CHECK(j == json({1, 2}));
// the readers reject a size this large before they announce
// it, so it can only reach start_array() directly
json k;
nlohmann::detail::json_sax_dom_parser<json, decltype(nlohmann::detail::input_adapter(v_ubjson))> sax2(k, false);
CHECK_FALSE(sax2.start_array((std::numeric_limits<std::size_t>::max)() - 1));
CHECK(sax2.is_errored());
} }
} }
} }
@@ -3400,3 +3428,72 @@ TEST_CASE("UBJSON optimized container type marker is validated for count 0")
} }
} }
} }
TEST_CASE("UBJSON optimized arrays from contiguous and non-contiguous input")
{
// contiguous input reads the numbers of an array with a type marker in
// one loop, other input number by number; both must give the same events,
// value, or error for the complete document and for every truncation of
// it, and stop at every event a SAX handler rejects
json document =
{
{"u8", {0, 200, 255}},
{"i8", {-5, 100, -128}},
{"i16", {-30000, 1000, 30000}},
{"i32", {-2000000000, 5, 70000}},
{"i64", {-9000000000000000000LL, 1, 5000000000LL}},
{"f64", {1.5, -0.1, 2.5}},
{"pairs", {{1.25, -3.75}, {1.25, -3.75}, json::array()}},
{"mixed", {1, "a", 2.5, nullptr}},
};
const std::vector<std::uint8_t> encoded = json::to_ubjson(document, true, true);
CHECK(json::from_ubjson(encoded) == document);
for (std::size_t length = 0; length <= encoded.size(); ++length)
{
CAPTURE(length)
const std::vector<std::uint8_t> contiguous(encoded.begin(), encoded.begin() + static_cast<std::ptrdiff_t>(length));
const std::list<std::uint8_t> bytewise(contiguous.begin(), contiguous.end());
CHECK(json::from_ubjson(contiguous, true, false).dump() == json::from_ubjson(bytewise.begin(), bytewise.end(), true, false).dump());
#if !defined(JSON_NOEXCEPTION)
std::string contiguous_error;
std::string bytewise_error;
try
{
json _;
_ = json::from_ubjson(contiguous);
}
catch (const json::exception& e)
{
contiguous_error = e.what();
}
try
{
json _;
_ = json::from_ubjson(bytewise.begin(), bytewise.end());
}
catch (const json::exception& e)
{
bytewise_error = e.what();
}
CHECK(contiguous_error == bytewise_error);
#endif
SaxEventLogger from_contiguous;
SaxEventLogger from_bytewise;
CHECK(json::sax_parse(contiguous, &from_contiguous, json::input_format_t::ubjson)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
CHECK(from_contiguous.events == from_bytewise.events);
}
for (int events = 0; events <= 80; ++events)
{
CAPTURE(events)
const std::list<std::uint8_t> bytewise(encoded.begin(), encoded.end());
SaxCountdown from_contiguous(events);
SaxCountdown from_bytewise(events);
CHECK(json::sax_parse(encoded, &from_contiguous, json::input_format_t::ubjson)
== json::sax_parse(bytewise.begin(), bytewise.end(), &from_bytewise, json::input_format_t::ubjson));
}
}
+38
View File
@@ -13,6 +13,7 @@ using nlohmann::json;
#include <algorithm> #include <algorithm>
#include <fstream> #include <fstream>
#include <random>
#include <string> #include <string>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
@@ -1631,3 +1632,40 @@ TEST_CASE("Unicode RFC 3629")
} }
DOCTEST_CLANG_SUPPRESS_WARNING_POP DOCTEST_CLANG_SUPPRESS_WARNING_POP
TEST_CASE("UTF-8 check of whole strings")
{
// valid_utf8_string() decides short strings with a word test and long ones
// 16 bytes at a time where a vector check is available; it must agree with
// the byte-wise check for every mix of ASCII, sequences of each length,
// ill-formed and truncated sequences, at every length and offset
const char* const pieces[] = // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
{
"a", "\x7F", "\xC3\xA9", "\xE2\x82\xAC", "\xE0\xA0\x80", "\xEF\xBF\xBF", "\xF0\x9F\x98\x80", "\xF4\x8F\xBF\xBF",
"\x80", "\xC0\xAF", "\xC3", "\xE2\x82", "\xED\xA0\x80", "\xF0\x9F", "\xF4\x90\x80\x80", "\xFF"
};
// std::mt19937's sequence is fixed by the standard; no distribution is used
std::mt19937 rng(5); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
std::size_t valid = 0;
for (int i = 0; i < 100000; ++i)
{
std::string s;
const std::size_t length = rng() % 100;
const auto ascii = static_cast<std::uint32_t>(rng() % 101);
while (s.size() < length)
{
// mostly well-formed pieces (the first eight), so that long valid
// strings are common
s += (rng() % 100 < ascii) ? std::string("x") : std::string(pieces[rng() % (rng() % 8 == 0 ? 16 : 8)]);
}
const auto* data = reinterpret_cast<const unsigned char*>(s.data());
const bool expected = nlohmann::detail::is_valid_utf8(s);
valid += expected ? 1 : 0;
CAPTURE(s)
CHECK(nlohmann::detail::valid_utf8_string(data, s.size()) == expected);
CHECK((nlohmann::detail::valid_utf8_prefix(data, s.size()) == s.size()) == expected);
}
// both outcomes are common enough to be tested
CHECK(valid > 20000);
CHECK(valid < 80000);
}
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more